import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../durum/proje_durumu.dart'; import '../../modeller/segment.dart'; import '../../servisler/analiz/waveform_olusturucu.dart'; import '../../servisler/proje_deposu.dart'; /// Basit dalga formu + segment ribbon (RMS verisi yoksa placeholder). class WaveformWidget extends ConsumerStatefulWidget { const WaveformWidget({super.key}); @override ConsumerState createState() => _WaveformWidgetState(); } class _WaveformWidgetState extends ConsumerState { double _playheadZamani = 0; bool _beatSnapAcik = false; // Sürükleme durumu String? _suruklenenSegmentId; bool _suruklenenSolKenar = false; double? _suruklenenYeniDeger; @override Widget build(BuildContext context) { final proje = ref.watch(projeProvider); final secId = ref.watch(secilenSegmentIdProvider); if (proje == null || proje.segmentler.isEmpty) { return const SizedBox(height: 100); } final theme = Theme.of(context); final toplamSure = proje.segmentler.last.bit; final rmsAkis = proje.analiz.rmsDegerleri; if (rmsAkis.isEmpty && proje.kaynak.sesPath.isNotEmpty && File(proje.kaynak.sesPath).existsSync()) { _waveformCikartTetikle(ref, proje); } final beatler = _beatlerHesapla(proje, toplamSure); return Container( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), border: Border.all(color: theme.dividerColor), ), child: Column( children: [ // Toolbar Container( height: 24, padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: theme.colorScheme.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(6)), ), child: Row( children: [ Icon(Icons.straighten, size: 14, color: theme.colorScheme.onSurface), const SizedBox(width: 4), const Text('Beat-snap', style: TextStyle(fontSize: 11)), const SizedBox(width: 4), Transform.scale( scale: 0.7, child: Switch( value: _beatSnapAcik, onChanged: beatler.isEmpty ? null : (v) => setState(() => _beatSnapAcik = v), ), ), const Spacer(), Text( '${_playheadZamani.toStringAsFixed(2)}s', style: TextStyle( fontSize: 10, color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), // Cetvel (tıklanabilir) LayoutBuilder(builder: (ctx, c) { return GestureDetector( behavior: HitTestBehavior.opaque, onTapDown: (d) => _cetvelTiklandi(d.localPosition, c.maxWidth, toplamSure, proje), child: SizedBox( height: 16, child: CustomPaint( size: const Size.fromHeight(16), painter: _CetvelCizici( toplamSure: toplamSure, renk: theme.colorScheme.outline, playheadX: c.maxWidth > 0 ? (_playheadZamani / toplamSure) * c.maxWidth : null, ), ), ), ); }), // Waveform (tıklanabilir + sürüklenebilir playhead) LayoutBuilder(builder: (ctx, c) { return SizedBox( height: 50, child: GestureDetector( behavior: HitTestBehavior.opaque, onTapDown: (d) => _waveformTiklandi( d.localPosition, c.maxWidth, toplamSure, proje), onPanUpdate: (d) => _waveformTiklandi( d.localPosition, c.maxWidth, toplamSure, proje), child: Stack( children: [ Positioned.fill( child: rmsAkis.isNotEmpty ? CustomPaint( painter: _WaveformCizici( rms: rmsAkis, renk: theme.colorScheme.primary, ), child: const SizedBox.expand(), ) : Center( child: Container( height: 30, decoration: BoxDecoration( gradient: LinearGradient(colors: [ theme.colorScheme.primary.withOpacity(0.3), theme.colorScheme.primary.withOpacity(0.6), theme.colorScheme.primary.withOpacity(0.3), ]), ), ), ), ), if (c.maxWidth > 0 && toplamSure > 0) Positioned( left: (_playheadZamani / toplamSure) * c.maxWidth - 1, top: 0, bottom: 0, width: 2, child: Container(color: Colors.red.shade500), ), ], ), ), ); }), // Segment ribbon SizedBox( height: 40, child: LayoutBuilder( builder: (ctx, c) { return Stack( children: proje.segmentler.map((s) { final basla = (_suruklenenSegmentId == s.id && _suruklenenSolKenar && _suruklenenYeniDeger != null) ? _suruklenenYeniDeger! : s.basla; final bit = (_suruklenenSegmentId == s.id && !_suruklenenSolKenar && _suruklenenYeniDeger != null) ? _suruklenenYeniDeger! : s.bit; final solOran = basla / toplamSure; final genisOran = (bit - basla) / toplamSure; final secili = s.id == secId; final bg = _segRenk(s, theme); return Positioned( left: c.maxWidth * solOran, width: (c.maxWidth * genisOran).clamp(2.0, c.maxWidth), top: 0, bottom: 0, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 1), child: Stack( children: [ Positioned.fill( child: GestureDetector( onTap: () => ref .read(secilenSegmentIdProvider.notifier) .state = s.id, child: Container( decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(4), border: secili ? Border.all( color: Colors.amber, width: 2) : null, ), padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'S${s.sira + 1}', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), if (s.analizMeta?.enerjiSeviyesi != null) Text( s.analizMeta!.enerjiSeviyesi!, style: const TextStyle( color: Colors.white70, fontSize: 8, ), ), ], ), ), ), ), // Sol kenar tutamacı Positioned( left: 0, top: 0, bottom: 0, width: 6, child: MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, child: GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: (_) { _suruklenenSegmentId = s.id; _suruklenenSolKenar = true; _suruklenenYeniDeger = s.basla; }, onPanUpdate: (d) => _kenarSurukle( s, true, d.delta.dx, c.maxWidth, toplamSure, beatler, proje), onPanEnd: (_) => _kenarSurukleBitti(proje, true), ), ), ), // Sağ kenar tutamacı Positioned( right: 0, top: 0, bottom: 0, width: 6, child: MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, child: GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: (_) { _suruklenenSegmentId = s.id; _suruklenenSolKenar = false; _suruklenenYeniDeger = s.bit; }, onPanUpdate: (d) => _kenarSurukle( s, false, d.delta.dx, c.maxWidth, toplamSure, beatler, proje), onPanEnd: (_) => _kenarSurukleBitti(proje, false), ), ), ), ], ), ), ); }).toList(), ); }, ), ), ], ), ); } // ---- Etkileşim yardımcıları ---- void _cetvelTiklandi( Offset pos, double w, double toplamSure, dynamic proje) { if (w <= 0) return; final t = (pos.dx / w * toplamSure).clamp(0.0, toplamSure); setState(() => _playheadZamani = t); final seg = _zamanaEnYakinSegment(proje.segmentler, t); if (seg != null) { ref.read(secilenSegmentIdProvider.notifier).state = seg.id; } } void _waveformTiklandi( Offset pos, double w, double toplamSure, dynamic proje) { if (w <= 0) return; final t = (pos.dx / w * toplamSure).clamp(0.0, toplamSure); setState(() => _playheadZamani = t); final seg = _zamanaEnYakinSegment(proje.segmentler, t); if (seg != null && seg.id != ref.read(secilenSegmentIdProvider)) { ref.read(secilenSegmentIdProvider.notifier).state = seg.id; } } Segment? _zamanaEnYakinSegment(List segmentler, double t) { for (final s in segmentler) { if (s.basla <= t && t <= s.bit) return s; } Segment? enYakin; double enKucukFark = double.infinity; for (final s in segmentler) { final orta = (s.basla + s.bit) / 2; final fark = (orta - t).abs(); if (fark < enKucukFark) { enKucukFark = fark; enYakin = s; } } return enYakin; } double _beatSnap(double t, List beatler) { if (!_beatSnapAcik || beatler.isEmpty) return t; double enYakin = beatler.first; double enKucukFark = (beatler.first - t).abs(); for (final b in beatler) { final f = (b - t).abs(); if (f < enKucukFark) { enKucukFark = f; enYakin = b; } } return enKucukFark <= 0.15 ? enYakin : t; } void _kenarSurukle(Segment s, bool sol, double deltaPx, double w, double toplamSure, List beatler, dynamic proje) { if (w <= 0) return; final deltaT = deltaPx / w * toplamSure; double yeniDeger = (_suruklenenYeniDeger ?? (sol ? s.basla : s.bit)) + deltaT; yeniDeger = _beatSnap(yeniDeger, beatler); // Sınır kontrolü if (sol) { final altSinir = _oncekiSegmentBaslangic(proje.segmentler, s); yeniDeger = yeniDeger.clamp(altSinir, s.bit - 0.05); } else { final ustSinir = _sonrakiSegmentBitis(proje.segmentler, s); yeniDeger = yeniDeger.clamp(s.basla + 0.05, ustSinir); } setState(() => _suruklenenYeniDeger = yeniDeger); } double _oncekiSegmentBaslangic(List segs, Segment s) { final idx = segs.indexWhere((e) => e.id == s.id); if (idx <= 0) return 0; return segs[idx - 1].basla + 0.05; } double _sonrakiSegmentBitis(List segs, Segment s) { final idx = segs.indexWhere((e) => e.id == s.id); if (idx < 0 || idx >= segs.length - 1) return segs.last.bit; return segs[idx + 1].bit - 0.05; } Future _kenarSurukleBitti(dynamic proje, bool sol) async { final segId = _suruklenenSegmentId; final yeniDeger = _suruklenenYeniDeger; setState(() { _suruklenenSegmentId = null; _suruklenenYeniDeger = null; }); if (segId == null || yeniDeger == null) return; final segs = List.from(proje.segmentler); final idx = segs.indexWhere((e) => e.id == segId); if (idx < 0) return; final s = segs[idx]; Segment guncellenen; if (sol) { guncellenen = s.copyWith( basla: yeniDeger, durum: s.durum == SegmentDurum.tamam ? SegmentDurum.bos : s.durum, ); segs[idx] = guncellenen; // Önceki segment chained mı? if (idx > 0 && (segs[idx - 1].bit - s.basla).abs() < 0.001) { segs[idx - 1] = segs[idx - 1].copyWith( bit: yeniDeger, durum: segs[idx - 1].durum == SegmentDurum.tamam ? SegmentDurum.bos : segs[idx - 1].durum, ); } } else { guncellenen = s.copyWith( bit: yeniDeger, durum: s.durum == SegmentDurum.tamam ? SegmentDurum.bos : s.durum, ); segs[idx] = guncellenen; // Sonraki segment chained mı? if (idx < segs.length - 1 && (segs[idx + 1].basla - s.bit).abs() < 0.001) { segs[idx + 1] = segs[idx + 1].copyWith( basla: yeniDeger, durum: segs[idx + 1].durum == SegmentDurum.tamam ? SegmentDurum.bos : segs[idx + 1].durum, ); } } final guncel = proje.copyWith(segmentler: segs); ref.read(projeProvider.notifier).ayarlaBypass(guncel); await ProjeDeposu.kaydet(guncel); } List _beatlerHesapla(dynamic proje, double toplamSure) { final analiz = proje.analiz; try { final dynamic beatList = analiz.beats; if (beatList is List && beatList.isNotEmpty) { return List.from(beatList.map((e) => (e as num).toDouble())); } } catch (_) {} double bpm = 0; try { bpm = (proje.global.bpm as num?)?.toDouble() ?? 0; } catch (_) { bpm = 0; } if (bpm <= 0) { try { bpm = (analiz.bpm as num?)?.toDouble() ?? 0; } catch (_) {} } if (bpm <= 0) return []; final t = 60.0 / bpm; final liste = []; for (double k = 0; k <= toplamSure; k += t) { liste.add(k); } return liste; } Color _segRenk(Segment s, ThemeData theme) { if (s.durum == SegmentDurum.tamam) { return theme.colorScheme.primary.withOpacity(0.7); } if (s.durum == SegmentDurum.uretiliyor) return Colors.amber.shade700; if (s.durum == SegmentDurum.kuyruk) return Colors.blue.shade700; if (s.durum == SegmentDurum.hata) return Colors.red.shade700; final sev = s.analizMeta?.enerjiSeviyesi; return switch (sev) { 'silence' => Colors.grey.shade700, 'low' => Colors.indigo.shade700, 'medium' => Colors.teal.shade700, 'high' => Colors.deepPurple.shade700, 'climax' => Colors.pink.shade700, _ => theme.colorScheme.outline, }; } } final Set _hesaplananProjeler = {}; void _waveformCikartTetikle(WidgetRef ref, dynamic proje) { if (_hesaplananProjeler.contains(proje.id)) return; _hesaplananProjeler.add(proje.id); Future.microtask(() async { final rms = await WaveformOlusturucu.rmsAkisi(proje.kaynak.sesPath); if (rms.isEmpty) return; final guncel = proje.copyWith( analiz: proje.analiz.copyWith( rmsZamanlari: List.generate(rms.length, (i) => i.toDouble()), rmsDegerleri: rms, guncellendi: DateTime.now(), ), ); await ProjeDeposu.kaydet(guncel); final aktifProje = ref.read(projeProvider); if (aktifProje?.id == proje.id) { ref.read(projeProvider.notifier).ayarlaBypass(guncel); } }); } class _CetvelCizici extends CustomPainter { final double toplamSure; final Color renk; final double? playheadX; _CetvelCizici({required this.toplamSure, required this.renk, this.playheadX}); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = renk ..strokeWidth = 1; final adim = toplamSure / 12; for (int i = 0; i <= 12; i++) { final x = i * size.width / 12; canvas.drawLine(Offset(x, 0), Offset(x, 6), paint); final t = (i * adim).toStringAsFixed(0); final tp = TextPainter( text: TextSpan( text: '${t}s', style: TextStyle(color: renk, fontSize: 9), ), textDirection: TextDirection.ltr, )..layout(); tp.paint(canvas, Offset(x + 2, 4)); } if (playheadX != null) { final ph = Paint() ..color = Colors.red.shade500 ..strokeWidth = 2; canvas.drawLine( Offset(playheadX!, 0), Offset(playheadX!, size.height), ph); } } @override bool shouldRepaint(covariant _CetvelCizici old) => old.playheadX != playheadX || old.toplamSure != toplamSure; } class _WaveformCizici extends CustomPainter { final List rms; final Color renk; _WaveformCizici({required this.rms, required this.renk}); @override void paint(Canvas canvas, Size size) { if (rms.isEmpty) return; final paint = Paint() ..color = renk ..strokeWidth = 1.5; final maxDb = rms.reduce((a, b) => a > b ? a : b); final minDb = rms.reduce((a, b) => a < b ? a : b); final range = (maxDb - minDb).abs() < 1 ? 1.0 : (maxDb - minDb).abs(); final cy = size.height / 2; for (int i = 0; i < rms.length; i++) { final x = i * size.width / rms.length; final norm = (rms[i] - minDb) / range; final h = norm * size.height * 0.9; canvas.drawLine(Offset(x, cy - h / 2), Offset(x, cy + h / 2), paint); } } @override bool shouldRepaint(_) => false; }