hap_weave 0.1.0
hap_weave: ^0.1.0 copied to clipboard
Cross-platform, capability-aware haptics for Flutter applications.
import 'package:flutter/material.dart';
import 'package:hap_weave/hap_weave.dart';
import 'custom_haptic_editor.dart';
import 'demo_strings.dart';
import 'preset_catalog.dart';
import 'preset_demo_card.dart';
void main() => runApp(const HapWeaveDemoApp());
class HapWeaveDemoApp extends StatefulWidget {
const HapWeaveDemoApp({super.key});
@override
State<HapWeaveDemoApp> createState() => _HapWeaveDemoAppState();
}
class _HapWeaveDemoAppState extends State<HapWeaveDemoApp> {
DemoLanguage? _language;
@override
Widget build(BuildContext context) {
final platformLocale = View.of(context).platformDispatcher.locale;
final language =
_language ??
(platformLocale.languageCode == 'ja'
? DemoLanguage.japanese
: DemoLanguage.english);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'HapWeave Lab',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF7357F6),
brightness: Brightness.light,
),
scaffoldBackgroundColor: const Color(0xFFF6F5F8),
cardTheme: const CardThemeData(
color: Colors.white,
elevation: 0,
margin: EdgeInsets.zero,
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFFF6F5F8),
foregroundColor: Color(0xFF201E2B),
elevation: 0,
scrolledUnderElevation: 0,
),
useMaterial3: true,
),
home: DemoShell(
language: language,
onLanguageChanged: (value) => setState(() => _language = value),
),
);
}
}
class DemoShell extends StatefulWidget {
const DemoShell({
required this.language,
required this.onLanguageChanged,
super.key,
});
final DemoLanguage language;
final ValueChanged<DemoLanguage> onLanguageChanged;
@override
State<DemoShell> createState() => _DemoShellState();
}
class _DemoShellState extends State<DemoShell> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
final language = widget.language;
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: [
DemoHomePage(
language: language,
onLanguageChanged: widget.onLanguageChanged,
),
CustomHapticEditorPage(
language: language,
onLanguageChanged: widget.onLanguageChanged,
),
],
),
bottomNavigationBar: SafeArea(
minimum: const EdgeInsets.fromLTRB(18, 6, 18, 12),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
color: Color(0x1A4B445A),
blurRadius: 24,
offset: Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(28),
child: NavigationBar(
height: 72,
backgroundColor: Colors.white,
indicatorColor: const Color(0xFFEAE5FF),
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
},
destinations: [
NavigationDestination(
icon: const Icon(Icons.grid_view_rounded),
selectedIcon: const Icon(Icons.grid_view_rounded),
label: language.text('Presets', 'プリセット'),
),
NavigationDestination(
icon: const Icon(Icons.design_services_outlined),
selectedIcon: const Icon(Icons.design_services_rounded),
label: language.text('Design', 'デザイン'),
),
],
),
),
),
),
);
}
}
class DemoHomePage extends StatefulWidget {
const DemoHomePage({
required this.language,
required this.onLanguageChanged,
super.key,
});
final DemoLanguage language;
final ValueChanged<DemoLanguage> onLanguageChanged;
@override
State<DemoHomePage> createState() => _DemoHomePageState();
}
class _DemoHomePageState extends State<DemoHomePage> {
HapticCapabilities? _capabilities;
PresetCategory? _category;
DemoLanguage get language => widget.language;
@override
void initState() {
super.initState();
_loadCapabilities();
}
Future<void> _loadCapabilities() async {
try {
final capabilities = await HapWeave.instance.capabilities;
if (mounted) setState(() => _capabilities = capabilities);
} catch (_) {}
}
Future<void> _play(HapticEffect effect) async {
try {
await HapWeave.instance.play(effect);
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(error.toString())));
}
}
}
@override
Widget build(BuildContext context) {
final visiblePresets = presetCatalog
.where((demo) => _category == null || demo.category == _category)
.toList(growable: false);
return Scaffold(
appBar: AppBar(
title: const Text(
'HapWeave',
style: TextStyle(fontWeight: FontWeight.w800),
),
actions: [
PopupMenuButton<DemoLanguage>(
tooltip: language.text('Change language', '言語を変更'),
initialValue: language,
onSelected: widget.onLanguageChanged,
icon: const Icon(Icons.language_rounded),
itemBuilder: (context) => const [
PopupMenuItem(
value: DemoLanguage.english,
child: Text('English'),
),
PopupMenuItem(value: DemoLanguage.japanese, child: Text('日本語')),
],
),
IconButton(
tooltip: language.text('Stop playback', '再生を停止'),
onPressed: HapWeave.instance.stop,
icon: const Icon(Icons.stop_circle_outlined),
),
],
),
body: ListView(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
children: [
_CapabilitiesCard(capabilities: _capabilities, language: language),
const SizedBox(height: 24),
_SectionTitle(
title: language.text('30 haptic presets', '30種類の振動プリセット'),
subtitle: language.text(
'Tap a tile to try it in a matching interface.',
'タイルを開き、用途に合ったUIで振動を体験できます。',
),
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_CategoryChip(
label: language.text('All 30', 'すべて 30'),
selected: _category == null,
onSelected: () => setState(() => _category = null),
),
for (final category in PresetCategory.values)
Padding(
padding: const EdgeInsets.only(left: 8),
child: _CategoryChip(
label: _categoryName(category, language),
selected: _category == category,
onSelected: () => setState(() => _category = category),
),
),
],
),
),
const SizedBox(height: 14),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 14,
crossAxisSpacing: 14,
childAspectRatio: 0.94,
),
itemCount: visiblePresets.length,
itemBuilder: (context, index) {
final demo = visiblePresets[index];
return PresetDemoCard(
key: ValueKey(demo.preset),
demo: demo,
language: language,
onPlay: () => _play(demo.preset),
);
},
),
],
),
);
}
}
String _categoryName(PresetCategory category, DemoLanguage language) =>
switch (category) {
PresetCategory.basics => language.text('Basics', '基本操作'),
PresetCategory.gestures => language.text('Gestures', 'ジェスチャー'),
PresetCategory.feedback => language.text('Feedback', '状態通知'),
PresetCategory.expressive => language.text('Expressive', '表現'),
};
class _CategoryChip extends StatelessWidget {
const _CategoryChip({
required this.label,
required this.selected,
required this.onSelected,
});
final String label;
final bool selected;
final VoidCallback onSelected;
@override
Widget build(BuildContext context) => FilterChip(
label: Text(label),
selected: selected,
onSelected: (_) => onSelected(),
);
}
class _CapabilitiesCard extends StatelessWidget {
const _CapabilitiesCard({required this.capabilities, required this.language});
final HapticCapabilities? capabilities;
final DemoLanguage language;
@override
Widget build(BuildContext context) {
final capabilities = this.capabilities;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
),
child: capabilities == null
? Row(
children: [
const SizedBox.square(
dimension: 18,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(width: 10),
Text(language.text('Checking this device…', '端末の対応状況を確認中…')),
],
)
: Row(
children: [
const Icon(Icons.phone_iphone_rounded, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
'${capabilities.platform.name} · ${capabilities.supportLevel.name}',
style: const TextStyle(fontWeight: FontWeight.w700),
),
),
Icon(
capabilities.supportsHaptics
? Icons.check_circle_rounded
: Icons.error_outline_rounded,
color: capabilities.supportsHaptics
? Colors.green
: Colors.orange,
),
],
),
);
}
}
class _SectionTitle extends StatelessWidget {
const _SectionTitle({required this.title, required this.subtitle});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w900),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
);
}