hap_weave 0.1.0 copy "hap_weave: ^0.1.0" to clipboard
hap_weave: ^0.1.0 copied to clipboard

Cross-platform, capability-aware haptics for Flutter applications.

example/lib/main.dart

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),
      ),
    ],
  );
}
0
likes
150
points
61
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Cross-platform, capability-aware haptics for Flutter applications.

Repository (GitHub)
View/report issues
Contributing

Topics

#haptics #vibration #feedback

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on hap_weave

Packages that implement hap_weave