synthkit 0.0.4 copy "synthkit: ^0.0.4" to clipboard
synthkit: ^0.0.4 copied to clipboard

Cross-platform Flutter synth plugin for note playback, envelopes, filters, and beat scheduling.

example/lib/main.dart

import 'package:synthkit/synthkit.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;

void main() {
  runApp(const SynthKitApp());
}

// ─── Theme ────────────────────────────────────────────────────────────────────

class SK {
  static const bg = Color(0xFF07090F);
  static const surface = Color(0xFF0D1117);
  static const card = Color(0xFF111827);
  static const cardHover = Color(0xFF161F30);
  static const border = Color(0xFF1E293B);
  static const borderBright = Color(0xFF2D3F5C);
  static const neon = Color(0xFF00E5B0);
  static const neonDim = Color(0x1A00E5B0);
  static const purple = Color(0xFF8B5CF6);
  static const purpleDim = Color(0x1A8B5CF6);
  static const amber = Color(0xFFF59E0B);
  static const amberDim = Color(0x1AF59E0B);
  static const rose = Color(0xFFF43F5E);
  static const roseDim = Color(0x1AF43F5E);
  static const text = Color(0xFFEFF6FF);
  static const textSub = Color(0xFF94A3B8);
  static const textMuted = Color(0xFF334155);
}

// ─── App ──────────────────────────────────────────────────────────────────────

class SynthKitApp extends StatelessWidget {
  const SynthKitApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SynthKit',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: SK.bg,
        colorScheme: const ColorScheme.dark(
          primary: SK.neon,
          surface: SK.surface,
        ),
      ),
      home: const _Root(),
    );
  }
}

// ─── Root shell ───────────────────────────────────────────────────────────────

class _Root extends StatefulWidget {
  const _Root();
  @override
  State<_Root> createState() => _RootState();
}

class _RootState extends State<_Root> {
  int _tab = 0;
  final _engine = SynthKitEngine();
  bool _initialized = false;
  bool _busy = false;
  String _status = 'Tap a control to initialize audio.';
  String? _backend;

  @override
  void dispose() {
    _engine.dispose();
    super.dispose();
  }

  void _setStatus(String s) {
    if (mounted) setState(() => _status = s);
  }

  Future<void> run(Future<void> Function() fn) async {
    if (_busy) return;
    setState(() => _busy = true);
    try {
      if (!_initialized) {
        await _engine.initialize(bpm: 112, masterVolume: 0.7);
        _initialized = true;
        _backend = (await _engine.backendName);
      }
      await fn();
    } catch (e) {
      _setStatus('Error: $e');
    } finally {
      if (mounted) setState(() => _busy = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final wide = MediaQuery.of(context).size.width >= 800;

    final pages = <Widget>[
      _LandingPage(onNavigate: (i) => setState(() => _tab = i)),
      BasicPage(engine: _engine, setStatus: _setStatus, run: run),
      UiSoundsPage(engine: _engine, setStatus: _setStatus, run: run),
      LofiPage(engine: _engine, setStatus: _setStatus, run: run),
    ];

    if (wide) {
      return Scaffold(
        body: Row(
          children: [
            _SideNav(
              tab: _tab,
              onTab: (i) => setState(() => _tab = i),
              status: _status,
              backend: _backend,
              busy: _busy,
            ),
            Expanded(child: pages[_tab]),
          ],
        ),
      );
    }

    return Scaffold(
      body: pages[_tab],
      bottomNavigationBar: _BottomBar(
        tab: _tab,
        onTab: (i) => setState(() => _tab = i),
        status: _status,
        busy: _busy,
      ),
    );
  }
}

// ─── Side nav ─────────────────────────────────────────────────────────────────

class _SideNav extends StatelessWidget {
  final int tab;
  final ValueChanged<int> onTab;
  final String status;
  final String? backend;
  final bool busy;
  const _SideNav({
    required this.tab,
    required this.onTab,
    required this.status,
    required this.backend,
    required this.busy,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 240,
      color: SK.surface,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const SizedBox(height: 40),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 24),
            child: Row(
              children: [
                Container(
                  width: 28,
                  height: 28,
                  decoration: BoxDecoration(
                    color: SK.neonDim,
                    borderRadius: BorderRadius.circular(6),
                    border: Border.all(color: SK.neon.withOpacity(0.4)),
                  ),
                  child: const Icon(Icons.graphic_eq, color: SK.neon, size: 16),
                ),
                const SizedBox(width: 12),
                const Text(
                  'synthkit',
                  style: TextStyle(
                    color: SK.text,
                    fontSize: 16,
                    fontWeight: FontWeight.w700,
                    letterSpacing: 1,
                    fontFamily: 'monospace',
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 6),
          const Padding(
            padding: EdgeInsets.only(left: 24),
            child: Text(
              'v0.0.1',
              style: TextStyle(
                color: SK.textMuted,
                fontSize: 11,
                fontFamily: 'monospace',
              ),
            ),
          ),
          const SizedBox(height: 32),
          const Padding(
            padding: EdgeInsets.symmetric(horizontal: 24),
            child: Text(
              'NAVIGATION',
              style: TextStyle(
                color: SK.textMuted,
                fontSize: 10,
                letterSpacing: 2,
              ),
            ),
          ),
          const SizedBox(height: 8),
          ..._kTabs.asMap().entries.map(
            (e) => _NavItem(
              icon: e.value.$1,
              label: e.value.$2,
              selected: tab == e.key,
              onTap: () => onTab(e.key),
            ),
          ),
          const Spacer(),
          Container(
            margin: const EdgeInsets.all(16),
            padding: const EdgeInsets.all(14),
            decoration: BoxDecoration(
              color: SK.card,
              borderRadius: BorderRadius.circular(10),
              border: Border.all(color: SK.border),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Container(
                      width: 6,
                      height: 6,
                      decoration: BoxDecoration(
                        color: busy ? SK.amber : SK.neon,
                        shape: BoxShape.circle,
                        boxShadow: [
                          BoxShadow(
                            color: (busy ? SK.amber : SK.neon).withOpacity(0.6),
                            blurRadius: 4,
                          ),
                        ],
                      ),
                    ),
                    const SizedBox(width: 8),
                    Text(
                      busy ? 'WORKING' : 'READY',
                      style: TextStyle(
                        color: busy ? SK.amber : SK.neon,
                        fontSize: 9,
                        letterSpacing: 2,
                        fontFamily: 'monospace',
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 8),
                Text(
                  status,
                  style: const TextStyle(
                    color: SK.textSub,
                    fontSize: 11,
                    height: 1.5,
                  ),
                ),
                if (backend != null) ...[
                  const SizedBox(height: 8),
                  Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 8,
                      vertical: 3,
                    ),
                    decoration: BoxDecoration(
                      color: SK.purpleDim,
                      borderRadius: BorderRadius.circular(4),
                      border: Border.all(color: SK.purple.withOpacity(0.3)),
                    ),
                    child: Text(
                      backend!,
                      style: const TextStyle(
                        color: SK.purple,
                        fontSize: 10,
                        fontFamily: 'monospace',
                      ),
                    ),
                  ),
                ],
              ],
            ),
          ),
          const SizedBox(height: 8),
        ],
      ),
    );
  }
}

const _kTabs = [
  (Icons.home_outlined, 'Overview'),
  (Icons.graphic_eq, 'Basic Synth'),
  (Icons.touch_app_outlined, 'UI Sounds'),
  (Icons.coffee_outlined, 'Lo-Fi Beat'),
];

class _NavItem extends StatelessWidget {
  final IconData icon;
  final String label;
  final bool selected;
  final VoidCallback onTap;
  const _NavItem({
    required this.icon,
    required this.label,
    required this.selected,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 180),
        margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11),
        decoration: BoxDecoration(
          color: selected ? SK.neonDim : Colors.transparent,
          borderRadius: BorderRadius.circular(8),
          border: Border.all(
            color: selected ? SK.neon.withOpacity(0.3) : Colors.transparent,
          ),
        ),
        child: Row(
          children: [
            Icon(icon, size: 16, color: selected ? SK.neon : SK.textSub),
            const SizedBox(width: 10),
            Text(
              label,
              style: TextStyle(
                color: selected ? SK.neon : SK.textSub,
                fontSize: 13,
                fontWeight: selected ? FontWeight.w600 : FontWeight.normal,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ─── Bottom bar (mobile) ──────────────────────────────────────────────────────

class _BottomBar extends StatelessWidget {
  final int tab;
  final ValueChanged<int> onTab;
  final String status;
  final bool busy;
  const _BottomBar({
    required this.tab,
    required this.onTab,
    required this.status,
    required this.busy,
  });

  @override
  Widget build(BuildContext context) {
    final bottom = MediaQuery.of(context).padding.bottom;
    return Container(
      decoration: BoxDecoration(
        color: SK.surface,
        border: Border(top: BorderSide(color: SK.border)),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(
            width: double.infinity,
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
            decoration: BoxDecoration(
              border: Border(bottom: BorderSide(color: SK.border)),
            ),
            child: Row(
              children: [
                Container(
                  width: 5,
                  height: 5,
                  decoration: BoxDecoration(
                    color: busy ? SK.amber : SK.neon,
                    shape: BoxShape.circle,
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    status,
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                    style: const TextStyle(color: SK.textSub, fontSize: 11),
                  ),
                ),
              ],
            ),
          ),
          Padding(
            padding: EdgeInsets.only(bottom: bottom),
            child: Row(
              children: _kTabs.asMap().entries.map((e) {
                final sel = tab == e.key;
                return Expanded(
                  child: GestureDetector(
                    onTap: () => onTab(e.key),
                    behavior: HitTestBehavior.opaque,
                    child: Padding(
                      padding: const EdgeInsets.symmetric(vertical: 11),
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Icon(
                            e.value.$1,
                            size: 20,
                            color: sel ? SK.neon : SK.textMuted,
                          ),
                          const SizedBox(height: 3),
                          Text(
                            e.value.$2.split(' ').first,
                            style: TextStyle(
                              fontSize: 10,
                              color: sel ? SK.neon : SK.textMuted,
                            ),
                          ),
                          const SizedBox(height: 3),
                          AnimatedContainer(
                            duration: const Duration(milliseconds: 200),
                            height: 2,
                            width: sel ? 20.0 : 0.0,
                            decoration: BoxDecoration(
                              color: SK.neon,
                              borderRadius: BorderRadius.circular(1),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                );
              }).toList(),
            ),
          ),
        ],
      ),
    );
  }
}

// ─── Landing / Overview ───────────────────────────────────────────────────────

class _LandingPage extends StatelessWidget {
  final ValueChanged<int> onNavigate;
  const _LandingPage({required this.onNavigate});

  @override
  Widget build(BuildContext context) {
    final w = MediaQuery.of(context).size.width;
    final maxW = w > 900 ? 800.0 : double.infinity;
    final hPad = w > 800 ? 48.0 : 24.0;

    return SingleChildScrollView(
      child: Center(
        child: ConstrainedBox(
          constraints: BoxConstraints(maxWidth: maxW),
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: hPad),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const SizedBox(height: 56),
                _HeroBanner(),
                const SizedBox(height: 48),

                // Feature pills
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: const [
                    _Pill('4 waveforms', SK.neon),
                    _Pill('ADSR envelope', SK.neon),
                    _Pill('Low-pass filter', SK.neon),
                    _Pill('Beat transport', SK.purple),
                    _Pill('Per-note velocity', SK.purple),
                    _Pill('Delayed triggers', SK.purple),
                    _Pill('Web · iOS · macOS', SK.amber),
                    _Pill('Android · Windows', SK.amber),
                  ],
                ),
                const SizedBox(height: 52),

                _H2('Platform Support'),
                const SizedBox(height: 16),
                const _PlatformTable(),
                const SizedBox(height: 52),

                _H2('Examples'),
                const SizedBox(height: 16),
                _ExGrid(onNavigate: onNavigate),
                const SizedBox(height: 52),

                _H2('Quick Start'),
                const SizedBox(height: 16),
                const _CodeCard(
                  code: r'''import 'package:synthkit/synthkit.dart';

final engine = SynthKitEngine();
await engine.initialize(bpm: 112, masterVolume: 0.7);

final synth = await engine.createSynth(
  const SynthKitSynthOptions(
    waveform: SynthKitWaveform.sawtooth,
    envelope: SynthKitEnvelope(
      attack: Duration(milliseconds: 8),
      decay: Duration(milliseconds: 140),
      sustain: 0.65,
      release: Duration(milliseconds: 260),
    ),
    filter: SynthKitFilter.lowPass(cutoffHz: 1600),
    volume: 0.75,
  ),
);

// Play a note immediately
await synth.triggerAttackRelease(
  SynthKitNote.parse('C4'),
  const Duration(milliseconds: 380),
);

// Schedule a pattern
await engine.transport.schedule(
  synth: synth,
  note: SynthKitNote.parse('G4'),
  beat: 0,
  durationBeats: 0.5,
);
await engine.transport.start();''',
                ),
                const SizedBox(height: 72),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// ─── Hero banner ─────────────────────────────────────────────────────────────

class _HeroBanner extends StatefulWidget {
  @override
  State<_HeroBanner> createState() => _HeroBannerState();
}

class _HeroBannerState extends State<_HeroBanner>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 5),
    )..repeat();
  }

  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Container(
              width: 8,
              height: 8,
              decoration: BoxDecoration(
                color: SK.neon,
                shape: BoxShape.circle,
                boxShadow: [
                  BoxShadow(color: SK.neon.withOpacity(0.7), blurRadius: 8),
                ],
              ),
            ),
            const SizedBox(width: 12),
            const Text(
              'pub.flutter-io.cn/packages/synthkit',
              style: TextStyle(
                color: SK.textSub,
                fontSize: 12,
                fontFamily: 'monospace',
              ),
            ),
          ],
        ),
        const SizedBox(height: 18),
        const Text(
          'synthkit',
          style: TextStyle(
            color: SK.text,
            fontSize: 52,
            fontWeight: FontWeight.w800,
            letterSpacing: -1,
            fontFamily: 'monospace',
          ),
        ),
        const SizedBox(height: 10),
        const Text(
          'Cross-platform Flutter synth plugin\n'
          'for note playback, envelopes,\n'
          'filters, and beat scheduling.',
          style: TextStyle(color: SK.textSub, fontSize: 17, height: 1.65),
        ),
        const SizedBox(height: 28),
        // Waveform — LayoutBuilder avoids infinite-width crash
        LayoutBuilder(
          builder: (ctx, box) {
            final w = box.maxWidth;
            if (w <= 0) return const SizedBox(height: 72);
            return SizedBox(
              height: 72,
              child: AnimatedBuilder(
                animation: _ctrl,
                builder: (_, __) => CustomPaint(
                  painter: _WavePainter(_ctrl.value, w),
                  size: Size(w, 72),
                ),
              ),
            );
          },
        ),
      ],
    );
  }
}

class _WavePainter extends CustomPainter {
  final double phase;
  final double w;
  _WavePainter(this.phase, this.w);

  @override
  void paint(Canvas canvas, Size size) {
    if (size.width <= 0 || size.height <= 0 || w <= 0) return;

    final line = Paint()
      ..color = SK.neon.withOpacity(0.9)
      ..strokeWidth = 1.5
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    final glow = Paint()
      ..color = SK.neon.withOpacity(0.15)
      ..strokeWidth = 9
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    final mid = size.height / 2;
    // Use a fixed step count based on captured width, never infinity
    final steps = w.clamp(1, 1200).toInt();

    final mp = Path();
    final gp = Path();
    bool first = true;

    for (int i = 0; i <= steps; i++) {
      final t = i / steps;
      final x = t * size.width;
      final env = math.sin(t * math.pi);
      final y =
          mid + math.sin(t * math.pi * 8 + phase * math.pi * 2) * 26 * env;
      if (first) {
        mp.moveTo(x, y);
        gp.moveTo(x, y);
        first = false;
      } else {
        mp.lineTo(x, y);
        gp.lineTo(x, y);
      }
    }

    canvas.drawPath(gp, glow);
    canvas.drawPath(mp, line);
  }

  @override
  bool shouldRepaint(covariant _WavePainter old) => old.phase != phase;
}

// ─── Landing helpers ──────────────────────────────────────────────────────────

class _Pill extends StatelessWidget {
  final String label;
  final Color color;
  const _Pill(this.label, this.color);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
      decoration: BoxDecoration(
        color: color.withOpacity(0.08),
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: color.withOpacity(0.3)),
      ),
      child: Text(label, style: TextStyle(color: color, fontSize: 12)),
    );
  }
}

Widget _H2(String t) => Text(
  t,
  style: const TextStyle(
    color: SK.text,
    fontSize: 22,
    fontWeight: FontWeight.w700,
  ),
);

class _PlatformTable extends StatelessWidget {
  const _PlatformTable();
  @override
  Widget build(BuildContext context) {
    const rows = [
      ('Web', 'Tone.js (loaded at runtime)', true),
      ('iOS', 'AVAudioEngine', true),
      ('macOS', 'AVAudioEngine', true),
      ('Android', 'AudioTrack', true),
      ('Windows', 'waveOut API', true),
      ('Linux', 'Not implemented', false),
    ];
    return Container(
      decoration: BoxDecoration(
        color: SK.card,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: SK.border),
      ),
      child: Column(
        children: rows.asMap().entries.map((e) {
          final last = e.key == rows.length - 1;
          final r = e.value;
          return Container(
            decoration: last
                ? null
                : BoxDecoration(
                    border: Border(bottom: BorderSide(color: SK.border)),
                  ),
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 13),
            child: Row(
              children: [
                SizedBox(
                  width: 88,
                  child: Text(
                    r.$1,
                    style: TextStyle(
                      color: r.$3 ? SK.text : SK.textMuted,
                      fontWeight: FontWeight.w600,
                      fontSize: 13,
                    ),
                  ),
                ),
                Expanded(
                  child: Text(
                    r.$2,
                    style: TextStyle(
                      color: r.$3 ? SK.textSub : SK.textMuted,
                      fontSize: 12,
                      fontFamily: 'monospace',
                    ),
                  ),
                ),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 8,
                    vertical: 3,
                  ),
                  decoration: BoxDecoration(
                    color: r.$3 ? SK.neonDim : SK.roseDim,
                    borderRadius: BorderRadius.circular(4),
                    border: Border.all(
                      color: (r.$3 ? SK.neon : SK.rose).withOpacity(0.3),
                    ),
                  ),
                  child: Text(
                    r.$3 ? 'Supported' : 'Planned',
                    style: TextStyle(
                      color: r.$3 ? SK.neon : SK.rose,
                      fontSize: 10,
                      fontFamily: 'monospace',
                    ),
                  ),
                ),
              ],
            ),
          );
        }).toList(),
      ),
    );
  }
}

class _ExGrid extends StatelessWidget {
  final ValueChanged<int> onNavigate;
  const _ExGrid({required this.onNavigate});

  @override
  Widget build(BuildContext context) {
    final items = [
      (
        1,
        Icons.graphic_eq,
        'Basic Synth',
        SK.neon,
        'Sawtooth oscillator · low-pass filter · ADSR envelope · one-shot and scheduled pattern playback.',
      ),
      (
        2,
        Icons.touch_app_outlined,
        'UI Sounds',
        SK.amber,
        'Click, hover, accept, cancel, error, success, and notification tones — ready to drop into any app.',
      ),
      (
        3,
        Icons.coffee_outlined,
        'Lo-Fi Beat',
        SK.purple,
        '8-bar Fmaj7–Em7–Dm7–Cmaj7 loop at 78 BPM with pad, bass, lead melody and arpeggio.',
      ),
    ];
    return Column(
      children: items
          .map(
            (ex) => Padding(
              padding: const EdgeInsets.only(bottom: 10),
              child: _ExCard(
                icon: ex.$2,
                title: ex.$3,
                color: ex.$4,
                desc: ex.$5,
                onTap: () => onNavigate(ex.$1),
              ),
            ),
          )
          .toList(),
    );
  }
}

class _ExCard extends StatefulWidget {
  final IconData icon;
  final String title;
  final Color color;
  final String desc;
  final VoidCallback onTap;
  const _ExCard({
    required this.icon,
    required this.title,
    required this.color,
    required this.desc,
    required this.onTap,
  });
  @override
  State<_ExCard> createState() => _ExCardState();
}

class _ExCardState extends State<_ExCard> {
  bool _h = false;
  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => setState(() => _h = true),
      onExit: (_) => setState(() => _h = false),
      child: GestureDetector(
        onTap: widget.onTap,
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 150),
          padding: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            color: _h ? SK.cardHover : SK.card,
            borderRadius: BorderRadius.circular(12),
            border: Border.all(
              color: _h ? widget.color.withOpacity(0.4) : SK.border,
            ),
          ),
          child: Row(
            children: [
              Container(
                width: 44,
                height: 44,
                decoration: BoxDecoration(
                  color: widget.color.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(10),
                  border: Border.all(color: widget.color.withOpacity(0.25)),
                ),
                child: Icon(widget.icon, color: widget.color, size: 20),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      widget.title,
                      style: const TextStyle(
                        color: SK.text,
                        fontSize: 14,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      widget.desc,
                      style: const TextStyle(
                        color: SK.textSub,
                        fontSize: 12,
                        height: 1.5,
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(width: 12),
              Icon(
                Icons.arrow_forward,
                size: 16,
                color: _h ? widget.color : SK.textMuted,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _CodeCard extends StatelessWidget {
  final String code;
  const _CodeCard({required this.code});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      decoration: BoxDecoration(
        color: const Color(0xFF060A10),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: SK.border),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            decoration: BoxDecoration(
              border: Border(bottom: BorderSide(color: SK.border)),
            ),
            child: Row(
              children: [
                _dot(SK.rose),
                const SizedBox(width: 6),
                _dot(SK.amber),
                const SizedBox(width: 6),
                _dot(SK.neon),
                const SizedBox(width: 16),
                const Text(
                  'main.dart',
                  style: TextStyle(
                    color: SK.textMuted,
                    fontSize: 12,
                    fontFamily: 'monospace',
                  ),
                ),
              ],
            ),
          ),
          SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            padding: const EdgeInsets.all(20),
            child: _SyntaxText(code),
          ),
        ],
      ),
    );
  }

  Widget _dot(Color c) => Container(
    width: 10,
    height: 10,
    decoration: BoxDecoration(
      color: c.withOpacity(0.7),
      shape: BoxShape.circle,
    ),
  );
}

class _SyntaxText extends StatelessWidget {
  final String code;
  const _SyntaxText(this.code);

  static const _kws = ['await', 'final', 'const', 'return', 'async'];
  static const _types = [
    'SynthKitSynthOptions',
    'SynthKitWaveform',
    'SynthKitEnvelope',
    'SynthKitFilter',
    'SynthKitNote',
    'SynthKitEngine',
    'SynthKitSynth',
    'SynthKitTransport',
    'Duration',
  ];

  @override
  Widget build(BuildContext context) {
    final spans = <InlineSpan>[];
    for (final line in code.split('\n')) {
      final trimmed = line.trimLeft();
      if (trimmed.startsWith('//')) {
        spans.add(
          TextSpan(
            text: '$line\n',
            style: const TextStyle(color: Color(0xFF4B6175)),
          ),
        );
      } else if (trimmed.startsWith("import '")) {
        spans.add(
          TextSpan(
            text: '$line\n',
            style: const TextStyle(color: Color(0xFF86EFAC)),
          ),
        );
      } else {
        spans.addAll(_parseLine(line));
        spans.add(const TextSpan(text: '\n'));
      }
    }
    return RichText(
      text: TextSpan(
        style: const TextStyle(
          fontSize: 13,
          height: 1.7,
          fontFamily: 'monospace',
        ),
        children: spans,
      ),
    );
  }

  List<TextSpan> _parseLine(String src) {
    final out = <TextSpan>[];
    var rest = src;
    while (rest.isNotEmpty) {
      // string literal
      final sm = RegExp(r"'[^']*'").matchAsPrefix(rest);
      if (sm != null) {
        out.add(
          TextSpan(
            text: sm.group(0),
            style: const TextStyle(color: Color(0xFFFBBF24)),
          ),
        );
        rest = rest.substring(sm.end);
        continue;
      }
      // types
      bool hit = false;
      for (final t in _types) {
        if (rest.startsWith(t) &&
            (rest.length == t.length ||
                !RegExp(r'\w').hasMatch(rest[t.length]))) {
          out.add(
            TextSpan(
              text: t,
              style: const TextStyle(color: Color(0xFF67E8F9)),
            ),
          );
          rest = rest.substring(t.length);
          hit = true;
          break;
        }
      }
      if (hit) continue;
      // keywords
      for (final kw in _kws) {
        if (rest.startsWith(kw) &&
            (rest.length == kw.length ||
                !RegExp(r'\w').hasMatch(rest[kw.length]))) {
          out.add(
            TextSpan(
              text: kw,
              style: const TextStyle(color: Color(0xFFC084FC)),
            ),
          );
          rest = rest.substring(kw.length);
          hit = true;
          break;
        }
      }
      if (hit) continue;
      // digit
      final isNum = RegExp(r'[0-9]').hasMatch(rest[0]);
      out.add(
        TextSpan(
          text: rest[0],
          style: TextStyle(
            color: isNum ? const Color(0xFF86EFAC) : const Color(0xFFCDD9E5),
          ),
        ),
      );
      rest = rest.substring(1);
    }
    return out;
  }
}

// ─── Basic Page ───────────────────────────────────────────────────────────────

class BasicPage extends StatefulWidget {
  final SynthKitEngine engine;
  final void Function(String) setStatus;
  final Future<void> Function(Future<void> Function()) run;
  const BasicPage({
    super.key,
    required this.engine,
    required this.setStatus,
    required this.run,
  });
  @override
  State<BasicPage> createState() => _BasicPageState();
}

class _BasicPageState extends State<BasicPage> {
  SynthKitSynth? _synth;

  Future<void> _ensure() async {
    _synth ??= await widget.engine.createSynth(
      const SynthKitSynthOptions(
        waveform: SynthKitWaveform.sawtooth,
        envelope: SynthKitEnvelope(
          attack: Duration(milliseconds: 8),
          decay: Duration(milliseconds: 140),
          sustain: 0.65,
          release: Duration(milliseconds: 260),
        ),
        filter: SynthKitFilter.lowPass(cutoffHz: 1600),
        volume: 0.75,
      ),
    );
  }

  Future<void> _oneShot() async {
    await widget.run(() async {
      await _ensure();
      await _synth!.triggerAttackRelease(
        SynthKitNote.parse('C4'),
        const Duration(milliseconds: 380),
      );
      widget.setStatus('Played C4 one-shot.');
    });
  }

  Future<void> _pattern() async {
    await widget.run(() async {
      await _ensure();
      await widget.engine.transport.stop(clearSequence: true);
      await widget.engine.transport.setBpm(112);
      const notes = [
        ('A3', 0.0, 0.5),
        ('C4', 1.0, 0.5),
        ('E4', 2.0, 0.5),
        ('G4', 3.0, 1.0),
      ];
      for (final n in notes) {
        await widget.engine.transport.schedule(
          synth: _synth!,
          note: SynthKitNote.parse(n.$1),
          beat: n.$2,
          durationBeats: n.$3,
        );
      }
      await widget.engine.transport.start();
      widget.setStatus('Playing A3–C4–E4–G4 at 112 BPM.');
    });
  }

  Future<void> _stop() async {
    await widget.run(() async {
      await widget.engine.transport.stop();
      widget.setStatus('Transport stopped.');
    });
  }

  @override
  Widget build(BuildContext context) {
    return _ExPage(
      icon: Icons.graphic_eq,
      color: SK.neon,
      title: 'Basic Synth',
      subtitle: 'Sawtooth · Low-pass filter · ADSR envelope',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _PropGrid(
            items: const [
              ('Waveform', 'sawtooth'),
              ('Filter', 'lowPass  1600 Hz'),
              ('Attack', '8 ms'),
              ('Decay', '140 ms'),
              ('Sustain', '65%'),
              ('Release', '260 ms'),
              ('Volume', '0.75'),
              ('BPM', '112'),
            ],
          ),
          const SizedBox(height: 24),
          _Label('PATTERN NOTES'),
          const SizedBox(height: 10),
          Row(
            children: ['A3', 'C4', 'E4', 'G4']
                .map(
                  (n) => Expanded(
                    child: Container(
                      margin: const EdgeInsets.symmetric(horizontal: 4),
                      padding: const EdgeInsets.symmetric(vertical: 14),
                      decoration: BoxDecoration(
                        color: SK.card,
                        borderRadius: BorderRadius.circular(8),
                        border: Border.all(color: SK.neon.withOpacity(0.2)),
                      ),
                      child: Text(
                        n,
                        textAlign: TextAlign.center,
                        style: const TextStyle(
                          color: SK.neon,
                          fontSize: 13,
                          fontWeight: FontWeight.w700,
                          fontFamily: 'monospace',
                        ),
                      ),
                    ),
                  ),
                )
                .toList(),
          ),
          const SizedBox(height: 24),
          _Label('CONTROLS'),
          const SizedBox(height: 12),
          _Btn(
            label: 'Play One-Shot  C4',
            icon: Icons.music_note,
            color: SK.neon,
            onTap: _oneShot,
          ),
          const SizedBox(height: 10),
          _Btn(
            label: 'Play Pattern',
            icon: Icons.queue_music,
            color: SK.purple,
            onTap: _pattern,
          ),
          const SizedBox(height: 10),
          _Btn(
            label: 'Stop',
            icon: Icons.stop,
            color: SK.rose,
            onTap: _stop,
            ghost: true,
          ),
        ],
      ),
    );
  }
}

// ─── UI Sounds Page ───────────────────────────────────────────────────────────

class UiSoundsPage extends StatefulWidget {
  final SynthKitEngine engine;
  final void Function(String) setStatus;
  final Future<void> Function(Future<void> Function()) run;
  const UiSoundsPage({
    super.key,
    required this.engine,
    required this.setStatus,
    required this.run,
  });
  @override
  State<UiSoundsPage> createState() => _UiSoundsPageState();
}

class _UiSoundsPageState extends State<UiSoundsPage> {
  SynthKitSynth? _s;

  Future<void> _ensure() async {
    _s ??= await widget.engine.createSynth(
      const SynthKitSynthOptions(waveform: SynthKitWaveform.sine, volume: 0.6),
    );
  }

  Future<void> _play(
    SynthKitSynthOptions opts,
    List<(String, int)> notes,
    String msg,
  ) async {
    await widget.run(() async {
      await _ensure();
      await _s!.update(opts);
      for (final n in notes) {
        await _s!.triggerAttackRelease(
          SynthKitNote.parse(n.$1),
          const Duration(milliseconds: 200),
          delay: Duration(milliseconds: n.$2),
        );
      }
      widget.setStatus(msg);
    });
  }

  @override
  Widget build(BuildContext context) {
    const e0 = SynthKitEnvelope(
      attack: Duration(milliseconds: 1),
      decay: Duration(milliseconds: 30),
      sustain: 0,
      release: Duration(milliseconds: 10),
    );
    const e1 = SynthKitEnvelope(
      attack: Duration(milliseconds: 1),
      decay: Duration(milliseconds: 15),
      sustain: 0,
      release: Duration(milliseconds: 10),
    );
    const e2 = SynthKitEnvelope(
      attack: Duration(milliseconds: 5),
      decay: Duration(milliseconds: 100),
      sustain: 0,
      release: Duration(milliseconds: 100),
    );
    const e3 = SynthKitEnvelope(
      attack: Duration(milliseconds: 10),
      decay: Duration(milliseconds: 100),
      sustain: 0,
      release: Duration(milliseconds: 100),
    );
    const e4 = SynthKitEnvelope(
      attack: Duration(milliseconds: 10),
      decay: Duration(milliseconds: 200),
      sustain: 0,
      release: Duration(milliseconds: 100),
    );
    const e5 = SynthKitEnvelope(
      attack: Duration(milliseconds: 5),
      decay: Duration(milliseconds: 150),
      sustain: 0,
      release: Duration(milliseconds: 200),
    );
    const e6 = SynthKitEnvelope(
      attack: Duration(milliseconds: 5),
      decay: Duration(milliseconds: 300),
      sustain: 0,
      release: Duration(milliseconds: 400),
    );

    final sounds = [
      (
        'Click',
        Icons.touch_app,
        SK.neon,
        SynthKitSynthOptions(
          waveform: SynthKitWaveform.sine,
          envelope: e0,
          volume: 0.5,
        ),
        [('C6', 0)],
        'Click — sine · C6',
      ),
      (
        'Hover',
        Icons.mouse_outlined,
        SK.neon,
        SynthKitSynthOptions(
          waveform: SynthKitWaveform.sine,
          envelope: e1,
          volume: 0.2,
        ),
        [('F5', 0)],
        'Hover — sine · F5',
      ),
      (
        'Accept',
        Icons.check_circle_outline,
        SK.neon,
        SynthKitSynthOptions(
          waveform: SynthKitWaveform.triangle,
          envelope: e2,
          filter: SynthKitFilter.lowPass(cutoffHz: 2000),
          volume: 0.6,
        ),
        [('C5', 0), ('E5', 100)],
        'Accept — triangle · C5 → E5',
      ),
      (
        'Cancel',
        Icons.cancel_outlined,
        SK.amber,
        SynthKitSynthOptions(
          waveform: SynthKitWaveform.square,
          envelope: e3,
          filter: SynthKitFilter.lowPass(cutoffHz: 600),
          volume: 0.4,
        ),
        [('E4', 0), ('C4', 120)],
        'Cancel — square · E4 → C4',
      ),
      (
        'Error',
        Icons.error_outline,
        SK.rose,
        SynthKitSynthOptions(
          waveform: SynthKitWaveform.sawtooth,
          envelope: e4,
          filter: SynthKitFilter.lowPass(cutoffHz: 800),
          volume: 0.6,
        ),
        [('Eb4', 0), ('A3', 160)],
        'Error — sawtooth · tritone',
      ),
      (
        'Success',
        Icons.star_outline,
        SK.neon,
        SynthKitSynthOptions(
          waveform: SynthKitWaveform.triangle,
          envelope: e5,
          filter: SynthKitFilter.lowPass(cutoffHz: 3000),
          volume: 0.5,
        ),
        [('C5', 0), ('E5', 100), ('G5', 200), ('C6', 300)],
        'Success — triangle arpeggio',
      ),
      (
        'Notification',
        Icons.notifications_outlined,
        SK.purple,
        SynthKitSynthOptions(
          waveform: SynthKitWaveform.sine,
          envelope: e6,
          volume: 0.5,
        ),
        [('A5', 0), ('E6', 150)],
        'Notification — sine · perfect fifth',
      ),
    ];

    return _ExPage(
      icon: Icons.touch_app_outlined,
      color: SK.amber,
      title: 'UI Sounds',
      subtitle: 'Tap any tile to audition the synthesized sound',
      child: Wrap(
        spacing: 10,
        runSpacing: 10,
        children: sounds
            .map(
              (s) => _SoundTile(
                label: s.$1,
                icon: s.$2,
                color: s.$3,
                onTap: () => _play(s.$4, s.$5, s.$6),
              ),
            )
            .toList(),
      ),
    );
  }
}

class _SoundTile extends StatefulWidget {
  final String label;
  final IconData icon;
  final Color color;
  final VoidCallback onTap;
  const _SoundTile({
    required this.label,
    required this.icon,
    required this.color,
    required this.onTap,
  });
  @override
  State<_SoundTile> createState() => _SoundTileState();
}

class _SoundTileState extends State<_SoundTile>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  late final Animation<double> _scale;
  bool _h = false;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 90),
    );
    _scale = Tween(
      begin: 1.0,
      end: 0.93,
    ).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
  }

  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  void _tap() async {
    await _ctrl.forward();
    widget.onTap();
    await _ctrl.reverse();
  }

  @override
  Widget build(BuildContext context) {
    final w = MediaQuery.of(context).size.width;
    final pad = w > 800 ? 96.0 : (w > 400 ? 48.0 : 48.0);
    final tileW = ((w - pad) / (w >= 600 ? 4.0 : 2.0) - 10.0).clamp(
      100.0,
      200.0,
    );

    return MouseRegion(
      onEnter: (_) => setState(() => _h = true),
      onExit: (_) => setState(() => _h = false),
      child: GestureDetector(
        onTap: _tap,
        child: ScaleTransition(
          scale: _scale,
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 150),
            width: tileW,
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: _h ? SK.cardHover : SK.card,
              borderRadius: BorderRadius.circular(10),
              border: Border.all(
                color: _h ? widget.color.withOpacity(0.4) : SK.border,
              ),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Container(
                  padding: const EdgeInsets.all(8),
                  decoration: BoxDecoration(
                    color: widget.color.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Icon(widget.icon, color: widget.color, size: 18),
                ),
                const SizedBox(height: 12),
                Text(
                  widget.label,
                  style: const TextStyle(
                    color: SK.text,
                    fontSize: 13,
                    fontWeight: FontWeight.w600,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// ─── Lo-Fi Page ───────────────────────────────────────────────────────────────

class LofiPage extends StatefulWidget {
  final SynthKitEngine engine;
  final void Function(String) setStatus;
  final Future<void> Function(Future<void> Function()) run;
  const LofiPage({
    super.key,
    required this.engine,
    required this.setStatus,
    required this.run,
  });
  @override
  State<LofiPage> createState() => _LofiPageState();
}

class _LofiPageState extends State<LofiPage>
    with SingleTickerProviderStateMixin {
  SynthKitSynth? _pad, _bass, _lead, _arp;
  bool _playing = false;
  late final AnimationController _pulse;

  @override
  void initState() {
    super.initState();
    _pulse = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 800),
    )..repeat(reverse: true);
  }

  @override
  void dispose() {
    _pulse.dispose();
    super.dispose();
  }

  Future<void> _ensure() async {
    if (_pad != null) return;
    _pad = await widget.engine.createSynth(
      const SynthKitSynthOptions(
        waveform: SynthKitWaveform.triangle,
        envelope: SynthKitEnvelope(
          attack: Duration(milliseconds: 200),
          decay: Duration(milliseconds: 400),
          sustain: 0.6,
          release: Duration(milliseconds: 1500),
        ),
        filter: SynthKitFilter.lowPass(cutoffHz: 800),
        volume: 0.35,
      ),
    );
    _bass = await widget.engine.createSynth(
      const SynthKitSynthOptions(
        waveform: SynthKitWaveform.sine,
        envelope: SynthKitEnvelope(
          attack: Duration(milliseconds: 50),
          decay: Duration(milliseconds: 400),
          sustain: 0.8,
          release: Duration(milliseconds: 800),
        ),
        filter: SynthKitFilter.lowPass(cutoffHz: 300),
        volume: 0.6,
      ),
    );
    _lead = await widget.engine.createSynth(
      const SynthKitSynthOptions(
        waveform: SynthKitWaveform.square,
        envelope: SynthKitEnvelope(
          attack: Duration(milliseconds: 20),
          decay: Duration(milliseconds: 200),
          sustain: 0.3,
          release: Duration(milliseconds: 600),
        ),
        filter: SynthKitFilter.lowPass(cutoffHz: 1200),
        volume: 0.2,
      ),
    );
    _arp = await widget.engine.createSynth(
      const SynthKitSynthOptions(
        waveform: SynthKitWaveform.triangle,
        envelope: SynthKitEnvelope(
          attack: Duration(milliseconds: 5),
          decay: Duration(milliseconds: 80),
          sustain: 0.1,
          release: Duration(milliseconds: 200),
        ),
        filter: SynthKitFilter.lowPass(cutoffHz: 1500),
        volume: 0.15,
      ),
    );
  }

  Future<void> _toggle() async {
    await widget.run(() async {
      if (_playing) {
        await widget.engine.transport.stop(clearSequence: true);
        setState(() => _playing = false);
        widget.setStatus('Stopped lo-fi sequence.');
        return;
      }
      await _ensure();
      await widget.engine.transport.stop(clearSequence: true);
      await widget.engine.transport.setBpm(78);

      final chords = [
        ['F3', 'A3', 'C4', 'E4'],
        ['E3', 'G3', 'B3', 'D4'],
        ['D3', 'F3', 'A3', 'C4'],
        ['C3', 'E3', 'G3', 'B3'],
      ];
      final bass = ['F2', 'E2', 'D2', 'C2'];

      for (int i = 0; i < 2; i++) {
        final off = i * 16.0;
        for (int c = 0; c < 4; c++) {
          final ms = off + c * 4.0;
          await widget.engine.transport.schedule(
            synth: _bass!,
            note: SynthKitNote.parse(bass[c]),
            beat: ms,
            durationBeats: 1.5,
          );
          await widget.engine.transport.schedule(
            synth: _bass!,
            note: SynthKitNote.parse(bass[c]),
            beat: ms + 2.5,
            durationBeats: 1.0,
          );
          for (final n in chords[c]) {
            await widget.engine.transport.schedule(
              synth: _pad!,
              note: SynthKitNote.parse(n),
              beat: ms,
              durationBeats: 1.0,
            );
            await widget.engine.transport.schedule(
              synth: _pad!,
              note: SynthKitNote.parse(n),
              beat: ms + 1.5,
              durationBeats: 0.5,
            );
            await widget.engine.transport.schedule(
              synth: _pad!,
              note: SynthKitNote.parse(n),
              beat: ms + 2.5,
              durationBeats: 1.5,
            );
          }
          for (int s = 0; s < 8; s++) {
            final raw = chords[c][s % 4];
            final oct = int.parse(raw[raw.length - 1]) + 1;
            await widget.engine.transport.schedule(
              synth: _arp!,
              note: SynthKitNote.parse(
                '${raw.substring(0, raw.length - 1)}$oct',
              ),
              beat: ms + s * 0.5,
              durationBeats: 0.25,
            );
          }
        }
        final ll = i == 0
            ? [
                ('A5', 0.0, 1.5),
                ('G5', 2.0, 0.25),
                ('F5', 2.5, 1.0),
                ('G5', 4.0, 1.0),
                ('E5', 5.5, 1.5),
                ('F5', 8.0, 1.0),
                ('D5', 9.5, 1.5),
                ('E5', 12.0, 1.0),
                ('C5', 13.5, 2.0),
              ]
            : [
                ('C6', 0.0, 1.5),
                ('A5', 2.0, 0.25),
                ('G5', 2.5, 1.0),
                ('G5', 4.0, 1.0),
                ('E5', 5.5, 1.5),
                ('D5', 8.0, 0.5),
                ('E5', 8.5, 0.5),
                ('F5', 9.0, 0.5),
                ('G5', 9.5, 1.5),
                ('C5', 12.0, 3.0),
              ];
        for (final l in ll) {
          await widget.engine.transport.schedule(
            synth: _lead!,
            note: SynthKitNote.parse(l.$1),
            beat: off + l.$2,
            durationBeats: l.$3,
          );
        }
      }
      await widget.engine.transport.start();
      setState(() => _playing = true);
      widget.setStatus('Playing 8-bar lo-fi at 78 BPM...');
    });
  }

  @override
  Widget build(BuildContext context) {
    const layers = [
      ('PAD', 'Triangle', 'LP 800 Hz', '0.35', SK.purple),
      ('BASS', 'Sine', 'LP 300 Hz', '0.60', SK.neon),
      ('LEAD', 'Square', 'LP 1200 Hz', '0.20', SK.amber),
      ('ARP', 'Triangle', 'LP 1500 Hz', '0.15', Color(0xFFF472B6)),
    ];
    const chords = [
      ('I', 'Fmaj7', 'F A C E'),
      ('vi', 'Em7', 'E G B D'),
      ('ii', 'Dm7', 'D F A C'),
      ('IV', 'Cmaj7', 'C E G B'),
    ];

    return _ExPage(
      icon: Icons.coffee_outlined,
      color: SK.purple,
      title: 'Lo-Fi Beat',
      subtitle: '8 bars · Fmaj7–Em7–Dm7–Cmaj7 · 78 BPM',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _Label('SYNTH LAYERS'),
          const SizedBox(height: 10),
          ...layers.map(
            (l) => Padding(
              padding: const EdgeInsets.only(bottom: 8),
              child: Container(
                padding: const EdgeInsets.symmetric(
                  horizontal: 16,
                  vertical: 12,
                ),
                decoration: BoxDecoration(
                  color: SK.card,
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: SK.border),
                ),
                child: Row(
                  children: [
                    Container(
                      width: 3,
                      height: 30,
                      decoration: BoxDecoration(
                        color: l.$5,
                        borderRadius: BorderRadius.circular(2),
                      ),
                    ),
                    const SizedBox(width: 14),
                    SizedBox(
                      width: 40,
                      child: Text(
                        l.$1,
                        style: TextStyle(
                          color: l.$5,
                          fontSize: 10,
                          fontWeight: FontWeight.w800,
                          letterSpacing: 1,
                          fontFamily: 'monospace',
                        ),
                      ),
                    ),
                    Expanded(
                      child: Text(
                        '${l.$2} · ${l.$3}',
                        style: const TextStyle(color: SK.textSub, fontSize: 12),
                      ),
                    ),
                    Text(
                      'vol ${l.$4}',
                      style: const TextStyle(
                        color: SK.textMuted,
                        fontSize: 11,
                        fontFamily: 'monospace',
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
          const SizedBox(height: 24),
          _Label('CHORD PROGRESSION'),
          const SizedBox(height: 10),
          Row(
            children: chords
                .map(
                  (c) => Expanded(
                    child: Container(
                      margin: const EdgeInsets.symmetric(horizontal: 3),
                      padding: const EdgeInsets.symmetric(
                        vertical: 12,
                        horizontal: 6,
                      ),
                      decoration: BoxDecoration(
                        color: SK.card,
                        borderRadius: BorderRadius.circular(8),
                        border: Border.all(color: SK.border),
                      ),
                      child: Column(
                        children: [
                          Text(
                            c.$1,
                            style: const TextStyle(
                              color: SK.textMuted,
                              fontSize: 10,
                              fontFamily: 'monospace',
                            ),
                          ),
                          const SizedBox(height: 4),
                          Text(
                            c.$2,
                            style: const TextStyle(
                              color: SK.purple,
                              fontSize: 12,
                              fontWeight: FontWeight.w700,
                            ),
                          ),
                          const SizedBox(height: 4),
                          Text(
                            c.$3,
                            style: const TextStyle(
                              color: SK.textMuted,
                              fontSize: 9,
                              fontFamily: 'monospace',
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                )
                .toList(),
          ),
          const SizedBox(height: 28),
          GestureDetector(
            onTap: _toggle,
            child: AnimatedBuilder(
              animation: _pulse,
              builder: (_, __) {
                final g = _playing ? (_pulse.value * 0.3 + 0.1) : 0.0;
                return AnimatedContainer(
                  duration: const Duration(milliseconds: 200),
                  width: double.infinity,
                  padding: const EdgeInsets.symmetric(vertical: 18),
                  decoration: BoxDecoration(
                    color: _playing ? SK.purple.withOpacity(0.12) : SK.card,
                    borderRadius: BorderRadius.circular(12),
                    border: Border.all(
                      color: _playing
                          ? SK.purple.withOpacity(0.5 + g)
                          : SK.borderBright,
                      width: 1.5,
                    ),
                    boxShadow: _playing
                        ? [
                            BoxShadow(
                              color: SK.purple.withOpacity(g * 0.5),
                              blurRadius: 24,
                              spreadRadius: 2,
                            ),
                          ]
                        : [],
                  ),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(
                        _playing
                            ? Icons.stop_rounded
                            : Icons.play_arrow_rounded,
                        color: _playing ? SK.purple : SK.text,
                        size: 24,
                      ),
                      const SizedBox(width: 10),
                      Text(
                        _playing ? 'STOP' : 'PLAY  LO-FI',
                        style: TextStyle(
                          color: _playing ? SK.purple : SK.text,
                          fontSize: 14,
                          fontWeight: FontWeight.w700,
                          letterSpacing: 2,
                          fontFamily: 'monospace',
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

// ─── Shared page scaffold ─────────────────────────────────────────────────────

class _ExPage extends StatelessWidget {
  final IconData icon;
  final Color color;
  final String title;
  final String subtitle;
  final Widget child;
  const _ExPage({
    required this.icon,
    required this.color,
    required this.title,
    required this.subtitle,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    final w = MediaQuery.of(context).size.width;
    final maxW = w > 900 ? 780.0 : double.infinity;
    final hPad = w > 800 ? 48.0 : 24.0;

    return SingleChildScrollView(
      child: Center(
        child: ConstrainedBox(
          constraints: BoxConstraints(maxWidth: maxW),
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: hPad, vertical: 40),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Container(
                      width: 40,
                      height: 40,
                      decoration: BoxDecoration(
                        color: color.withOpacity(0.1),
                        borderRadius: BorderRadius.circular(10),
                        border: Border.all(color: color.withOpacity(0.25)),
                      ),
                      child: Icon(icon, color: color, size: 20),
                    ),
                    const SizedBox(width: 16),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            title,
                            style: const TextStyle(
                              color: SK.text,
                              fontSize: 20,
                              fontWeight: FontWeight.w700,
                            ),
                          ),
                          Text(
                            subtitle,
                            style: const TextStyle(
                              color: SK.textSub,
                              fontSize: 12,
                              fontFamily: 'monospace',
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 28),
                const Divider(color: SK.border),
                const SizedBox(height: 28),
                child,
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// ─── Shared widgets ───────────────────────────────────────────────────────────

class _PropGrid extends StatelessWidget {
  final List<(String, String)> items;
  const _PropGrid({required this.items});

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: SK.card,
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: SK.border),
      ),
      child: Column(
        children: items.asMap().entries.map((e) {
          final last = e.key == items.length - 1;
          return Container(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            decoration: last
                ? null
                : BoxDecoration(
                    border: Border(bottom: BorderSide(color: SK.border)),
                  ),
            child: Row(
              children: [
                Text(
                  e.value.$1,
                  style: const TextStyle(color: SK.textSub, fontSize: 12),
                ),
                const Spacer(),
                Text(
                  e.value.$2,
                  style: const TextStyle(
                    color: SK.neon,
                    fontSize: 12,
                    fontFamily: 'monospace',
                  ),
                ),
              ],
            ),
          );
        }).toList(),
      ),
    );
  }
}

Widget _Label(String t) => Text(
  t,
  style: const TextStyle(color: SK.textMuted, fontSize: 10, letterSpacing: 2),
);

class _Btn extends StatefulWidget {
  final String label;
  final IconData icon;
  final Color color;
  final VoidCallback onTap;
  final bool ghost;
  const _Btn({
    required this.label,
    required this.icon,
    required this.color,
    required this.onTap,
    this.ghost = false,
  });
  @override
  State<_Btn> createState() => _BtnState();
}

class _BtnState extends State<_Btn> {
  bool _h = false;
  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => setState(() => _h = true),
      onExit: (_) => setState(() => _h = false),
      child: GestureDetector(
        onTap: widget.onTap,
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 150),
          padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
          decoration: BoxDecoration(
            color: widget.ghost
                ? Colors.transparent
                : widget.color.withOpacity(_h ? 0.18 : 0.1),
            borderRadius: BorderRadius.circular(8),
            border: Border.all(
              color: widget.color.withOpacity(widget.ghost ? 0.4 : 0.3),
            ),
          ),
          child: Row(
            children: [
              Icon(widget.icon, color: widget.color, size: 17),
              const SizedBox(width: 12),
              Text(
                widget.label,
                style: TextStyle(
                  color: widget.color,
                  fontSize: 13,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
1
likes
0
points
25
downloads

Publisher

unverified uploader

Weekly Downloads

Cross-platform Flutter synth plugin for note playback, envelopes, filters, and beat scheduling.

Homepage
Repository (GitHub)
View/report issues

Topics

#audio #synth #music #tonejs #flutter-plugin

License

unknown (license)

Dependencies

flutter, flutter_web_plugins, plugin_platform_interface, web

More

Packages that depend on synthkit

Packages that implement synthkit