import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/tema.dart'; import '../cekirdek/ffmpeg_servisi.dart'; import 'direktor_state.dart'; import 'efekt_katalogu.dart'; final olcekProvider = StateProvider((ref) => 55); // px / saniye final seciliSahneProvider = StateProvider((ref) => null); final playheadProvider = StateProvider((ref) => 0); // saniye final seciliKatmanKlipProvider = StateProvider<(int, int)?>((ref) => null); final timelineYukseklikProvider = StateProvider((ref) => 240); /// Timeline içeriği (zaman 0 = sol kenar) — mutlak fare konumu için. final GlobalKey _timelineKey = GlobalKey(); double _fareZamani(Offset global, double olcek) { final box = _timelineKey.currentContext?.findRenderObject() as RenderBox?; if (box == null) return 0; return box.globalToLocal(global).dx / olcek; } /// Katman yüksekliği çarpanı (S/M/L) — kullanıcı ayarlar. final trackBoyuProvider = StateProvider((ref) => 1.0); double _carpan = 1.0; const _cetvelY = 26.0; double get _anaKlipY => 64.0 * _carpan; double get _miniTrackY => 34.0 * _carpan; double get _sesAnaY => 36.0 * _carpan; const _fxY = 15.0; // efekt şeridi (ana track altı) const _baslikGen = 116.0; /// ULTRA profesyonel çok-katmanlı timeline — CapCut Pro / LTX Director üstü. /// Çok track, splitter, beat-snap, marker, undo/redo, kısayollar, trim. class ProfesyonelTimeline extends ConsumerWidget { const ProfesyonelTimeline({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final d = ref.watch(direktorProvider); if (d.sahneler.isEmpty && d.ekKatmanlar.isEmpty) { return const SizedBox.shrink(); } final yukseklik = ref.watch(timelineYukseklikProvider); final olcek = ref.watch(olcekProvider); _carpan = ref.watch(trackBoyuProvider); final toplam = _toplamSure(d); final genislik = (toplam * olcek + 60).clamp(300.0, 200000.0); // Track sırası (üst→alt): overlay video/yazı, ANA V1, ana ses, ek sesler final ustler = []; // ekKatmanlar index — video/yazı final altlar = []; // ekKatmanlar index — ses for (var i = 0; i < d.ekKatmanlar.length; i++) { (d.ekKatmanlar[i].tip == KatmanTipi.ses ? altlar : ustler).add(i); } final icYukseklik = _cetvelY + ustler.length * _miniTrackY + _anaKlipY + _fxY + _sesAnaY + altlar.length * _miniTrackY + 20; return Card( child: Column( mainAxisSize: MainAxisSize.min, children: [ // Splitter — timeline yüksekliğini ayarla MouseRegion( cursor: SystemMouseCursors.resizeUpDown, child: GestureDetector( onVerticalDragUpdate: (e) => ref .read(timelineYukseklikProvider.notifier) .state = (yukseklik - e.delta.dy).clamp(140.0, 520.0), child: Container( height: 9, alignment: Alignment.center, color: Colors.transparent, child: Container( width: 52, height: 3.5, decoration: BoxDecoration( color: Theme.of(context).dividerColor, borderRadius: BorderRadius.circular(2)), ), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 6), child: _KisayolSarici( child: Column( mainAxisSize: MainAxisSize.min, children: [ const _AracCubugu(), const SizedBox(height: 4), SizedBox( height: yukseklik, child: SingleChildScrollView( child: SizedBox( height: icYukseklik, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Sol: track başlıkları SizedBox( width: _baslikGen, child: _TrackBasliklari( ustler: ustler, altlar: altlar), ), // Sağ: yatay scroll içerik Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( width: genislik, height: icYukseklik, child: Stack( key: _timelineKey, clipBehavior: Clip.none, children: [ Positioned( top: 0, left: 0, right: 0, child: _Cetvel( olcek: olcek, toplam: toplam)), // Üst overlay track'ler for (var u = 0; u < ustler.length; u++) Positioned( top: _cetvelY + u * _miniTrackY + 2, left: 0, width: genislik, height: _miniTrackY - 4, child: _KatmanSeridi( ki: ustler[u], olcek: olcek), ), // ANA video track Positioned( top: _cetvelY + ustler.length * _miniTrackY + 2, left: 0, width: genislik, height: _anaKlipY, child: _AnaVideoTrack(olcek: olcek), ), // FX şeridi (uygulanan efektler ayrı sırada) Positioned( top: _cetvelY + ustler.length * _miniTrackY + _anaKlipY + 4, left: 0, width: genislik, height: _fxY, child: _FxSeridi(olcek: olcek), ), // Ana ses (şarkı) Positioned( top: _cetvelY + ustler.length * _miniTrackY + _anaKlipY + _fxY + 8, left: 0, child: _AnaSesSeridi( olcek: olcek, toplam: toplam, sarki: d.sarkiYolu), ), // Alt ek ses track'leri for (var a = 0; a < altlar.length; a++) Positioned( top: _cetvelY + ustler.length * _miniTrackY + _anaKlipY + _fxY + _sesAnaY + 12 + a * _miniTrackY, left: 0, width: genislik, height: _miniTrackY - 4, child: _KatmanSeridi( ki: altlar[a], olcek: olcek), ), // Markerlar + Playhead (tam yükseklik) _Markerlar(olcek: olcek, h: icYukseklik), _Playhead( olcek: olcek, toplam: toplam, h: icYukseklik), ], ), ), ), ), ], ), ), ), ), ], ), ), ), ], ), ); } } double _toplamSure(DirektorDurumu d) { var t = d.sahneler.fold(0, (a, s) => a + s.sure); for (final k in d.ekKatmanlar) { for (final klip in k.klipler) { if (klip.baslangic + klip.sure > t) t = klip.baslangic + klip.sure; } } return t < 1 ? 1 : t; } // ───────────────────────── Klavye kısayolları ───────────────────────── class _KisayolSarici extends ConsumerWidget { const _KisayolSarici({required this.child}); final Widget child; @override Widget build(BuildContext context, WidgetRef ref) { final m = ref.read(direktorProvider.notifier); return Focus( canRequestFocus: true, onKeyEvent: (node, e) { if (e is! KeyDownEvent) return KeyEventResult.ignored; final secili = ref.read(seciliSahneProvider); final ph = ref.read(playheadProvider); final ctrl = HardwareKeyboard.instance.isControlPressed; final k = e.logicalKey; if (ctrl && k == LogicalKeyboardKey.keyZ) { m.geriAl(); } else if (ctrl && k == LogicalKeyboardKey.keyY) { m.yinele(); } else if (ctrl && k == LogicalKeyboardKey.keyD && secili != null) { m.sahneCogalt(secili); } else if (k == LogicalKeyboardKey.keyS && secili != null) { _playheadOraniylaBol(ref, m, secili, ph); } else if (k == LogicalKeyboardKey.delete) { final fxSec = ref.read(seciliFxProvider); final kSec = ref.read(seciliKatmanKlipProvider); if (fxSec != null) { m.spanSil(fxSec); ref.read(seciliFxProvider.notifier).state = null; } else if (kSec != null) { m.katmanKlipSil(kSec.$1, kSec.$2); ref.read(seciliKatmanKlipProvider.notifier).state = null; } else if (secili != null) { m.sahneSil(secili); ref.read(seciliSahneProvider.notifier).state = null; } } else if (k == LogicalKeyboardKey.keyM) { m.markerEkleKaldir(ph); } else if (k == LogicalKeyboardKey.bracketLeft && secili != null) { m.trimOncesi(secili, ph); } else if (k == LogicalKeyboardKey.bracketRight && secili != null) { m.trimSonrasi(secili, ph); } else if (k == LogicalKeyboardKey.equal || k == LogicalKeyboardKey.add) { ref.read(olcekProvider.notifier).update((o) => (o * 1.25).clamp(10, 400)); } else if (k == LogicalKeyboardKey.minus) { ref.read(olcekProvider.notifier).update((o) => (o / 1.25).clamp(10, 400)); } else { return KeyEventResult.ignored; } return KeyEventResult.handled; }, child: GestureDetector( onTap: () => Focus.of(context).requestFocus(), child: child, ), ); } } void _playheadOraniylaBol( WidgetRef ref, DirektorMotor m, int i, double ph) { final sahneler = ref.read(direktorProvider).sahneler; var bas = 0.0; for (var k = 0; k < i && k < sahneler.length; k++) { bas += sahneler[k].sure; } if (i >= sahneler.length) return; final oran = ((ph - bas) / sahneler[i].sure).clamp(0.1, 0.9); m.sahneBol(i, oran); } // ───────────────────────── Araç çubuğu ───────────────────────── class _AracCubugu extends ConsumerWidget { const _AracCubugu(); @override Widget build(BuildContext context, WidgetRef ref) { final m = ref.read(direktorProvider.notifier); final d = ref.watch(direktorProvider); final olcek = ref.watch(olcekProvider); final ph = ref.watch(playheadProvider); final secili = ref.watch(seciliSahneProvider); final toplam = _toplamSure(d); return Row(children: [ const Icon(Icons.view_timeline, size: 15, color: FikoRenkler.neonAmber), const SizedBox(width: 5), const Text('Timeline', style: TextStyle(fontSize: 11.5, fontWeight: FontWeight.w800)), const SizedBox(width: 8), _Btn(Icons.zoom_out, 'Uzaklaş (-)', () => ref.read(olcekProvider.notifier).state = (olcek / 1.25).clamp(10, 400)), _Btn(Icons.zoom_in, 'Yakınlaş (+)', () => ref.read(olcekProvider.notifier).state = (olcek * 1.25).clamp(10, 400)), // Katman yüksekliği S/M/L for (final (et, c) in const [('S', 0.7), ('M', 1.0), ('L', 1.45)]) Tooltip( message: 'Katman boyu $et', child: InkWell( borderRadius: BorderRadius.circular(4), onTap: () => ref.read(trackBoyuProvider.notifier).state = c, child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: ref.watch(trackBoyuProvider) == c ? BoxDecoration( color: FikoRenkler.neonAmber.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4)) : null, child: Text(et, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w800, color: ref.watch(trackBoyuProvider) == c ? FikoRenkler.neonAmber : null)), ), ), ), _Ayir(), _Btn(Icons.undo, 'Geri al (Ctrl+Z)', m.geriAl), _Btn(Icons.redo, 'Yinele (Ctrl+Y)', m.yinele), _Ayir(), _Tgl(Icons.attractions, 'Mıknatıs (snap)', d.snapAcik, m.snapDegistir), _Tgl(Icons.music_note, 'Beat-snap (vuruşa yapış)', d.beatSnap, m.beatSnapDegistir, renk: FikoRenkler.neonCyan), _Btn(Icons.flag, 'Marker ekle/kaldır (M)', () => m.markerEkleKaldir(ph)), _Ayir(), if (secili != null) ...[ _Btn(Icons.content_cut, 'Playhead\'den böl (S)', () => _playheadOraniylaBol(ref, m, secili, ph)), _Btn(Icons.keyboard_tab, 'Öncesini kes [', () => m.trimOncesi(secili, ph)), Transform.flip( flipX: true, child: _Btn(Icons.keyboard_tab, 'Sonrasını kes ]', () => m.trimSonrasi(secili, ph)), ), _Btn(Icons.copy, 'Çoğalt (Ctrl+D)', () => m.sahneCogalt(secili)), _Btn(Icons.delete_outline, 'Sil (Del)', () { m.sahneSil(secili); ref.read(seciliSahneProvider.notifier).state = null; }, renk: Theme.of(context).colorScheme.error), _Ayir(), ], // Katman ekle MenuAnchor( builder: (c, ctrl, _) => _Btn(Icons.library_add, 'Katman ekle', () => ctrl.isOpen ? ctrl.close() : ctrl.open()), menuChildren: [ MenuItemButton( leadingIcon: const Icon(Icons.video_library, size: 15), onPressed: () => m.katmanEkle(KatmanTipi.video), child: const Text('Video/Resim katmanı (üst)')), MenuItemButton( leadingIcon: const Icon(Icons.title, size: 15), onPressed: () => m.katmanEkle(KatmanTipi.yazi), child: const Text('Yazı katmanı (üst)')), MenuItemButton( leadingIcon: const Icon(Icons.audiotrack, size: 15), onPressed: () => m.katmanEkle(KatmanTipi.ses), child: const Text('Ses katmanı (alt)')), ], ), const Spacer(), if (d.bpm > 0) Padding( padding: const EdgeInsets.only(right: 8), child: Text('♪${d.bpm.round()}', style: const TextStyle( fontSize: 10, color: FikoRenkler.neonCyan)), ), Text('${_mmss(ph)} / ${_mmss(toplam)}', style: const TextStyle(fontSize: 10.5, color: FikoRenkler.neonAmber)), ]); } } // ───────────────────────── Track başlıkları ───────────────────────── class _TrackBasliklari extends ConsumerWidget { const _TrackBasliklari({required this.ustler, required this.altlar}); final List ustler; final List altlar; @override Widget build(BuildContext context, WidgetRef ref) { final d = ref.watch(direktorProvider); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: _cetvelY), for (final ki in ustler) SizedBox( height: _miniTrackY, child: _KatmanBaslik(ki: ki, katman: d.ekKatmanlar[ki])), SizedBox( height: _anaKlipY + 2, child: _BaslikKutu( ad: 'V1 • Sahneler', renk: FikoRenkler.neonAmber, ikon: Icons.movie, ), ), SizedBox( height: _fxY + 4, child: _BaslikKutu( ad: 'FX', renk: FikoRenkler.neonPembe, ikon: Icons.auto_fix_high, ), ), SizedBox( height: _sesAnaY + 8, child: _BaslikKutu( ad: 'A1 • Şarkı', renk: FikoRenkler.neonCyan, ikon: Icons.music_note, ), ), for (final ki in altlar) SizedBox( height: _miniTrackY, child: _KatmanBaslik(ki: ki, katman: d.ekKatmanlar[ki])), ], ); } } class _BaslikKutu extends StatelessWidget { const _BaslikKutu({required this.ad, required this.renk, required this.ikon}); final String ad; final Color renk; final IconData ikon; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(right: 6, bottom: 3), padding: const EdgeInsets.symmetric(horizontal: 6), decoration: BoxDecoration( color: renk.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(6), border: Border.all(color: renk.withValues(alpha: 0.3)), ), child: Row(children: [ Icon(ikon, size: 12, color: renk), const SizedBox(width: 4), Expanded( child: Text(ad, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 9.5, fontWeight: FontWeight.w700, color: renk)), ), ]), ); } } class _KatmanBaslik extends ConsumerWidget { const _KatmanBaslik({required this.ki, required this.katman}); final int ki; final Katman katman; @override Widget build(BuildContext context, WidgetRef ref) { final m = ref.read(direktorProvider.notifier); final renk = switch (katman.tip) { KatmanTipi.video => FikoRenkler.neonPembe, KatmanTipi.yazi => FikoRenkler.neonYesil, KatmanTipi.ses => FikoRenkler.neonMor, }; return Container( margin: const EdgeInsets.only(right: 6, bottom: 3), padding: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( color: renk.withValues(alpha: 0.07), borderRadius: BorderRadius.circular(6), border: Border.all(color: renk.withValues(alpha: 0.28)), ), child: Row(children: [ Expanded( child: Text(katman.ad, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 9, fontWeight: FontWeight.w700, color: renk)), ), _MiniIkon( katman.gorunur ? Icons.visibility : Icons.visibility_off, katman.gorunur ? renk : null, () => m.katmanDegistir(ki, (k) => k.gorunur = !k.gorunur)), _MiniIkon( katman.kilitli ? Icons.lock : Icons.lock_open, katman.kilitli ? renk : null, () => m.katmanDegistir(ki, (k) => k.kilitli = !k.kilitli)), _MiniIkon(Icons.add, null, () => _klipEkleDialog(context, ref, ki)), _MiniIkon(Icons.close, null, () => m.katmanSil(ki)), ]), ); } Future _klipEkleDialog( BuildContext context, WidgetRef ref, int ki) async { final m = ref.read(direktorProvider.notifier); final ph = ref.read(playheadProvider); if (katman.tip == KatmanTipi.yazi) { final ctrl = TextEditingController(); await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Yazı Ekle'), content: TextField( controller: ctrl, autofocus: true, decoration: const InputDecoration(labelText: 'Metin')), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Vazgeç')), FilledButton( onPressed: () { if (ctrl.text.trim().isNotEmpty) { m.katmanKlipEkle(ki, KatmanKlip(baslangic: ph, sure: 3, yazi: ctrl.text.trim())); } Navigator.pop(ctx); }, child: const Text('Ekle'), ), ], ), ); return; } final tip = katman.tip == KatmanTipi.ses ? FileType.audio : FileType.media; final s = await FilePicker.platform.pickFiles(type: tip); final yol = s?.files.firstOrNull?.path; if (yol == null || !File(yol).existsSync()) return; double sure = 4; if (katman.tip == KatmanTipi.ses || yol.toLowerCase().endsWith('.mp4') || yol.toLowerCase().endsWith('.mov')) { sure = await FfmpegServisi.sesSuresi(yol) ?? 4; } m.katmanKlipEkle(ki, KatmanKlip(baslangic: ph, sure: sure, dosya: yol)); } } // ───────────────────────── Cetvel + Marker + Playhead ───────────────────────── class _Cetvel extends ConsumerWidget { const _Cetvel({required this.olcek, required this.toplam}); final double olcek; final double toplam; @override Widget build(BuildContext context, WidgetRef ref) { final d = ref.watch(direktorProvider); final aralik = olcek > 90 ? 1 : (olcek > 45 ? 2 : (olcek > 22 ? 5 : 10)); final cocuklar = []; // Saniye işaretleri for (var s = 0; s <= toplam.ceil(); s += aralik) { cocuklar.add(Positioned( left: s * olcek, top: 0, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Container(width: 1, height: 7, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4)), Text(_mmss(s.toDouble()), style: TextStyle(fontSize: 7.5, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5))), ]), )); } // Beat tikleri (BPM) if (d.beatSnap && d.bpm > 0) { final beat = 60.0 / d.bpm; final bar = beat * 4; for (var t = 0.0; t <= toplam; t += beat) { final barMi = (t / bar) - (t / bar).floorToDouble() < 0.001; cocuklar.add(Positioned( left: t * olcek, top: barMi ? 14 : 18, child: Container( width: 1, height: barMi ? 10 : 6, color: FikoRenkler.neonCyan.withValues(alpha: barMi ? 0.65 : 0.3)), )); } } return GestureDetector( behavior: HitTestBehavior.translucent, onTapDown: (e) { final t = _fareZamani(e.globalPosition, olcek).clamp(0.0, toplam); ref.read(playheadProvider.notifier).state = ref.read(direktorProvider.notifier).snapla(t, olcek); }, child: SizedBox(height: _cetvelY, child: Stack(children: cocuklar)), ); } } class _Markerlar extends ConsumerWidget { const _Markerlar({required this.olcek, required this.h}); final double olcek; final double h; @override Widget build(BuildContext context, WidgetRef ref) { final markerlar = ref.watch(direktorProvider.select((d) => d.markerlar)); return Stack(clipBehavior: Clip.none, children: [ for (final mark in markerlar) Positioned( left: mark * olcek - 5, top: 0, child: GestureDetector( onTap: () => ref.read(direktorProvider.notifier).markerEkleKaldir(mark), child: Column(children: [ const Icon(Icons.flag, size: 11, color: FikoRenkler.neonYesil), Container(width: 1, height: h - 14, color: FikoRenkler.neonYesil.withValues(alpha: 0.35)), ]), ), ), ]); } } class _Playhead extends ConsumerWidget { const _Playhead( {required this.olcek, required this.toplam, required this.h}); final double olcek; final double toplam; final double h; @override Widget build(BuildContext context, WidgetRef ref) { final ph = ref.watch(playheadProvider); return Positioned( left: ph * olcek - 6, top: 0, width: 12, height: h, child: GestureDetector( behavior: HitTestBehavior.translucent, onHorizontalDragUpdate: (d) { final t = _fareZamani(d.globalPosition, olcek).clamp(0.0, toplam); ref.read(playheadProvider.notifier).state = ref.read(direktorProvider.notifier).snapla(t, olcek); }, child: Column(children: [ Container(width: 11, height: 11, decoration: const BoxDecoration( color: FikoRenkler.neonAmber, borderRadius: BorderRadius.all(Radius.circular(2)))), Expanded(child: Container(width: 1.5, color: FikoRenkler.neonAmber)), ]), ), ); } } // ───────────────────────── ANA video track ───────────────────────── class _AnaVideoTrack extends ConsumerWidget { const _AnaVideoTrack({required this.olcek}); final double olcek; @override Widget build(BuildContext context, WidgetRef ref) { final sahneler = ref.watch(direktorProvider.select((d) => d.sahneler)); final varsayilanGecis = ref.watch(direktorProvider.select((d) => d.varsayilanGecis)); final secili = ref.watch(seciliSahneProvider); final klipler = []; var t = 0.0; for (var i = 0; i < sahneler.length; i++) { final s = sahneler[i]; final sol = t * olcek; final gen = (s.sure * olcek).clamp(14.0, 200000.0); t += s.sure; klipler.add(Positioned( left: sol, top: 0, width: gen, height: _anaKlipY, child: _AnaKlip( indeks: i, sahne: s, secili: secili == i, gecis: i == 0 ? null : (s.gecis.isEmpty ? varsayilanGecis : s.gecis), olcek: olcek), )); } // Boş alana medya bırakılırsa sona ekle return DragTarget( onWillAcceptWithDetails: (d) => d.data is MedyaTasima, onAcceptWithDetails: (d) { final veri = d.data; if (veri is MedyaTasima) { ref.read(direktorProvider.notifier).medyaTimelineEkle(veri.indeks, playhead: ref.read(playheadProvider)); } }, builder: (context, adaylar, _) => GestureDetector( behavior: HitTestBehavior.translucent, onTapUp: (d) { final t2 = _fareZamani(d.globalPosition, olcek).clamp(0.0, 100000.0); ref.read(playheadProvider.notifier).state = ref.read(direktorProvider.notifier).snapla(t2, olcek); ref.read(seciliSahneProvider.notifier).state = null; ref.read(seciliKatmanKlipProvider.notifier).state = null; }, child: Stack(clipBehavior: Clip.none, children: [ Positioned.fill( child: Container( decoration: BoxDecoration( color: adaylar.isNotEmpty ? FikoRenkler.neonCyan.withValues(alpha: 0.12) : Theme.of(context) .colorScheme .surface .withValues(alpha: 0.4), borderRadius: BorderRadius.circular(6), ), ), ), ...klipler, ]), ), ); } } class _AnaKlip extends ConsumerStatefulWidget { const _AnaKlip( {required this.indeks, required this.sahne, required this.secili, required this.gecis, required this.olcek}); final int indeks; final Sahne sahne; final bool secili; final String? gecis; final double olcek; @override ConsumerState<_AnaKlip> createState() => _AnaKlipState(); } class _AnaKlipState extends ConsumerState<_AnaKlip> { double _dragT = -1; bool _birakmaHedefi = false; @override Widget build(BuildContext context) { final m = ref.read(direktorProvider.notifier); final s = widget.sahne; final renk = switch (s.durum) { SahneDurum.tamam => FikoRenkler.neonYesil, SahneDurum.hata => Theme.of(context).colorScheme.error, SahneDurum.bos => Colors.grey, _ => FikoRenkler.neonAmber, }; // Kütüphaneden sürüklenen efekt/geçiş/medyayı bu klibe bırak return DragTarget( onWillAcceptWithDetails: (d) { final ok = d.data is EfektTasima || d.data is GecisTasima || d.data is MedyaTasima; if (ok) setState(() => _birakmaHedefi = true); return ok; }, onLeave: (_) => setState(() => _birakmaHedefi = false), onAcceptWithDetails: (d) { setState(() => _birakmaHedefi = false); final veri = d.data; if (veri is EfektTasima) { if (!s.efektler.contains(veri.id)) { m.sahneEfektDegistir(widget.indeks, veri.id); } } else if (veri is GecisTasima) { m.sahneGecisAyarla(widget.indeks, veri.id); } else if (veri is MedyaTasima) { // Medyayı bu klibin konumuna araya ekle m.medyaTimelineEkle(veri.indeks, konum: widget.indeks, playhead: ref.read(playheadProvider)); } ref.read(seciliSahneProvider.notifier).state = widget.indeks; }, builder: (context, adaylar, _) => _govde(context, m, s, _birakmaHedefi || adaylar.isNotEmpty ? FikoRenkler.neonPembe : renk), ); } Widget _govde( BuildContext context, DirektorMotor m, Sahne s, Color renk) { return GestureDetector( onTap: () { ref.read(seciliSahneProvider.notifier).state = widget.indeks; ref.read(seciliKatmanKlipProvider.notifier).state = null; }, onSecondaryTapUp: (e) { ref.read(seciliSahneProvider.notifier).state = widget.indeks; _sagTikMenu(context, m, e.globalPosition); }, onHorizontalDragStart: (_) => ref.read(seciliSahneProvider.notifier).state = widget.indeks, onHorizontalDragUpdate: (d) => _dragT = _fareZamani(d.globalPosition, widget.olcek), onHorizontalDragEnd: (_) { if (_dragT < 0) return; final sahneler = ref.read(direktorProvider).sahneler; var t = 0.0, hedef = 0; for (var i = 0; i < sahneler.length; i++) { hedef = i; if (_dragT < t + sahneler[i].sure / 2) break; t += sahneler[i].sure; } m.sahneTasima(widget.indeks, hedef); ref.read(seciliSahneProvider.notifier).state = hedef; _dragT = -1; }, child: Stack(clipBehavior: Clip.none, children: [ Container( margin: const EdgeInsets.symmetric(horizontal: 1), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), border: Border.all( color: widget.secili ? FikoRenkler.neonCyan : renk.withValues(alpha: 0.6), width: widget.secili ? 2 : 1), image: s.imageDosya.isNotEmpty && File(s.imageDosya).existsSync() ? DecorationImage( image: FileImage(File(s.imageDosya)), fit: BoxFit.cover, colorFilter: ColorFilter.mode( Colors.black .withValues(alpha: s.gizli ? 0.72 : 0.2), BlendMode.darken)) : null, color: Theme.of(context).colorScheme.surface, ), clipBehavior: Clip.antiAlias, child: Align( alignment: Alignment.bottomLeft, child: Container( padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1), color: Colors.black54, child: Text( '${s.gizli ? '🚫 ' : ''}${widget.indeks + 1} • ${s.sure.toStringAsFixed(1)}s' '${s.kaynakBas > 0.01 ? ' ✂' : ''}', style: const TextStyle(fontSize: 8, color: Colors.white)), ), ), ), if (s.efektler.isNotEmpty) Positioned( top: 2, right: 4, child: GestureDetector( onTap: () => _klipEfektMenu(context, m, s), child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: FikoRenkler.neonPembe.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(4)), child: Text('✨${s.efektler.length}', style: const TextStyle(fontSize: 8, color: Colors.white)), ), ), ), if (widget.gecis != null && widget.gecis != 'none') Positioned( left: -7, top: _anaKlipY / 2 - 8, child: GestureDetector( onTap: () => _gecisMenu(context, m), child: Container( width: 16, height: 16, decoration: const BoxDecoration( color: FikoRenkler.neonPembe, shape: BoxShape.circle), child: const Icon(Icons.swap_horiz, size: 11, color: Colors.white), ), ), ), // Sağ kenar boyutlandırma (snap'li) Positioned( right: -2, top: 0, bottom: 0, width: 12, child: MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, child: GestureDetector( behavior: HitTestBehavior.translucent, onHorizontalDragUpdate: (d) { final sahneler = ref.read(direktorProvider).sahneler; var bas = 0.0; for (var i = 0; i < widget.indeks && i < sahneler.length; i++) { bas += sahneler[i].sure; } final ham = _fareZamani(d.globalPosition, widget.olcek); final snap = m.snapla(ham, widget.olcek); m.sahneSureAyarla(widget.indeks, snap - bas); }, child: Center( child: Container( width: 3, height: 24, decoration: BoxDecoration( color: widget.secili ? FikoRenkler.neonCyan : Colors.white24, borderRadius: BorderRadius.circular(2))), ), ), ), ), ]), ); } void _gecisMenu(BuildContext context, DirektorMotor m) { showMenu( context: context, position: const RelativeRect.fromLTRB(200, 300, 200, 100), items: [ for (final (id, et) in direktorGecisleri) PopupMenuItem( value: id, height: 30, child: Text(et, style: const TextStyle(fontSize: 12))), ], ).then((v) { if (v != null) m.sahneGecisAyarla(widget.indeks, v); }); } /// SAĞ TIK — sahnenin tam kontrol menüsü (editörden her şey). void _sagTikMenu(BuildContext context, DirektorMotor m, Offset poz) { final s = widget.sahne; showMenu( context: context, position: RelativeRect.fromLTRB(poz.dx, poz.dy, poz.dx + 1, poz.dy + 1), items: [ _mi('prompt', Icons.edit_note, 'Prompt düzenle/kaydet'), _mi('resim_yukle', Icons.add_photo_alternate, 'Resim ver (dosyadan)'), _mi('resim_uret', Icons.image, 'Resmi üret (Flux)'), if (s.imageInputAdi.isNotEmpty) _mi('video_uret', Icons.movie, 'Videoyu üret (bu resimden)'), _mi('ikisi', Icons.auto_awesome, 'Resim + Video üret'), const PopupMenuDivider(height: 6), _mi('gizle', s.gizli ? Icons.visibility : Icons.visibility_off, s.gizli ? 'Görüntüyü aç' : 'Görüntüyü kes (gizle)'), _mi('cogalt', Icons.copy, 'Çoğalt'), _mi('sil', Icons.delete_outline, 'Sil'), ], ).then((v) async { if (v == null || !context.mounted) return; switch (v) { case 'prompt': final ctrl = TextEditingController(text: s.prompt); await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text('Sahne ${widget.indeks + 1} — Prompt'), content: TextField( controller: ctrl, autofocus: true, minLines: 2, maxLines: 5, decoration: const InputDecoration(hintText: 'Sahne promptu...')), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Vazgeç')), FilledButton( onPressed: () { m.sahnePrompt(widget.indeks, ctrl.text); Navigator.pop(ctx); }, child: const Text('Kaydet')), ], ), ); case 'resim_yukle': final sec = await FilePicker.platform.pickFiles(type: FileType.image); final yol = sec?.files.firstOrNull?.path; if (yol != null && File(yol).existsSync()) { await m.sahneResimAta(widget.indeks, yol); } case 'resim_uret': m.sahneUret(widget.indeks, sadeceResim: true); case 'video_uret': m.sahneUret(widget.indeks, sadeceVideo: true); case 'ikisi': m.sahneUret(widget.indeks); case 'gizle': m.sahneGizleToggle(widget.indeks); case 'cogalt': m.sahneCogalt(widget.indeks); case 'sil': m.sahneSil(widget.indeks); ref.read(seciliSahneProvider.notifier).state = null; } }); } PopupMenuItem _mi(String v, IconData i, String t) => PopupMenuItem( value: v, height: 32, child: Row(children: [ Icon(i, size: 15), const SizedBox(width: 8), Text(t, style: const TextStyle(fontSize: 12)), ]), ); void _klipEfektMenu(BuildContext context, DirektorMotor m, Sahne s) { showMenu( context: context, position: const RelativeRect.fromLTRB(260, 340, 200, 100), items: [ for (final id in s.efektler) PopupMenuItem( value: id, height: 30, child: Row(children: [ const Icon(Icons.close, size: 13, color: Colors.redAccent), const SizedBox(width: 6), Text(VideoEfekt.bul(id)?.etiket ?? id, style: const TextStyle(fontSize: 12)), ]), ), const PopupMenuItem( value: '__hepsi__', height: 30, child: Text('Hepsini temizle', style: TextStyle(fontSize: 12, color: Colors.redAccent)), ), ], ).then((v) { if (v == null) return; if (v == '__hepsi__') { m.sahneEfektTemizle(widget.indeks); } else { m.sahneEfektDegistir(widget.indeks, v); } }); } } // ───────────────────────── Katman şeridi (overlay/ses/yazı) ───────────────────────── class _KatmanSeridi extends ConsumerWidget { const _KatmanSeridi({required this.ki, required this.olcek}); final int ki; final double olcek; @override Widget build(BuildContext context, WidgetRef ref) { final katman = ref.watch(direktorProvider.select((d) => d.ekKatmanlar[ki])); final secili = ref.watch(seciliKatmanKlipProvider); final renk = switch (katman.tip) { KatmanTipi.video => FikoRenkler.neonPembe, KatmanTipi.yazi => FikoRenkler.neonYesil, KatmanTipi.ses => FikoRenkler.neonMor, }; return Stack(clipBehavior: Clip.none, children: [ Positioned.fill( child: Container( decoration: BoxDecoration( color: renk.withValues(alpha: katman.gorunur ? 0.06 : 0.02), borderRadius: BorderRadius.circular(5), border: Border.all(color: renk.withValues(alpha: 0.22)), ), ), ), for (var i = 0; i < katman.klipler.length; i++) _KatmanKlipW( ki: ki, kli: i, klip: katman.klipler[i], renk: renk, olcek: olcek, kilitli: katman.kilitli, secili: secili != null && secili.$1 == ki && secili.$2 == i, tip: katman.tip, ), ]); } } class _KatmanKlipW extends ConsumerWidget { const _KatmanKlipW( {required this.ki, required this.kli, required this.klip, required this.renk, required this.olcek, required this.kilitli, required this.secili, required this.tip}); final int ki, kli; final KatmanKlip klip; final Color renk; final double olcek; final bool kilitli, secili; final KatmanTipi tip; @override Widget build(BuildContext context, WidgetRef ref) { final m = ref.read(direktorProvider.notifier); final etiket = tip == KatmanTipi.yazi ? klip.yazi : klip.dosya.split(Platform.pathSeparator).last; return Positioned( left: klip.baslangic * olcek, top: 1, bottom: 1, width: (klip.sure * olcek).clamp(12.0, 200000.0), child: GestureDetector( onTap: () { ref.read(seciliKatmanKlipProvider.notifier).state = (ki, kli); ref.read(seciliSahneProvider.notifier).state = null; }, onDoubleTap: tip == KatmanTipi.yazi ? () => _yaziDuzenle(context, m) : null, onSecondaryTapUp: (e) { showMenu( context: context, position: RelativeRect.fromLTRB(e.globalPosition.dx, e.globalPosition.dy, e.globalPosition.dx + 1, e.globalPosition.dy + 1), items: [ PopupMenuItem( value: 'kes', height: 32, child: Row(children: [ Icon(klip.aktif ? Icons.visibility_off : Icons.visibility, size: 15), const SizedBox(width: 8), Text( klip.aktif ? (tip == KatmanTipi.ses ? 'Sesi kes' : 'Görüntüyü kes') : 'Tekrar aç', style: const TextStyle(fontSize: 12)), ]), ), if (tip == KatmanTipi.ses) const PopupMenuItem( value: 'ses', height: 32, child: Row(children: [ Icon(Icons.graphic_eq, size: 15), SizedBox(width: 8), Text('Ses: seviye + fade', style: TextStyle(fontSize: 12)), ]), ), if (tip == KatmanTipi.video) const PopupMenuItem( value: 'overlay', height: 32, child: Row(children: [ Icon(Icons.tune, size: 15), SizedBox(width: 8), Text('Konum / boyut / opaklık', style: TextStyle(fontSize: 12)), ]), ), const PopupMenuItem( value: 'sil', height: 32, child: Row(children: [ Icon(Icons.delete_outline, size: 15), SizedBox(width: 8), Text('Sil', style: TextStyle(fontSize: 12)), ]), ), ], ).then((v) { if (v == 'kes') m.katmanKlipAktifToggle(ki, kli); if (v == 'sil') m.katmanKlipSil(ki, kli); if (v == 'ses' && context.mounted) _sesAyarDialog(context, ref, m); if (v == 'overlay' && context.mounted) { _overlayAyarDialog(context, ref, m); } }); }, onHorizontalDragUpdate: kilitli ? null : (d) { final t = _fareZamani(d.globalPosition, olcek) - klip.sure / 2; m.katmanKlipGuncelle(ki, kli, (k) => k.baslangic = m.snapla(t, olcek).clamp(0, 100000)); }, child: Stack(clipBehavior: Clip.none, children: [ Opacity( opacity: klip.aktif ? 1.0 : 0.35, child: Container( decoration: BoxDecoration( color: renk.withValues(alpha: secili ? 0.45 : 0.3), borderRadius: BorderRadius.circular(4), border: Border.all( color: secili ? Colors.white : renk, width: secili ? 1.5 : 1), ), padding: const EdgeInsets.symmetric(horizontal: 4), alignment: Alignment.centerLeft, child: Text( '${klip.aktif ? '' : '🚫 '}' '${tip == KatmanTipi.ses && klip.fadeIn > 0.01 ? '◢ ' : ''}' '$etiket' '${tip == KatmanTipi.ses && klip.fadeOut > 0.01 ? ' ◣' : ''}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 8.5, color: Colors.white)), ), ), // sağ kenar boyutlandırma if (!kilitli) Positioned( right: -2, top: 0, bottom: 0, width: 9, child: MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, child: GestureDetector( behavior: HitTestBehavior.translucent, onHorizontalDragUpdate: (d) { final ham = _fareZamani(d.globalPosition, olcek); final snap = m.snapla(ham, olcek); m.katmanKlipGuncelle(ki, kli, (k) => k.sure = (snap - k.baslangic).clamp(0.2, 100000)); }, child: const SizedBox(), ), ), ), ]), ), ); } /// Ses klibi: seviye (kazanç) + fade in/out ayar paneli. void _sesAyarDialog(BuildContext context, WidgetRef ref, DirektorMotor m) { showDialog( context: context, builder: (ctx) => Consumer(builder: (c, ref2, _) { final katman = ref2.watch(direktorProvider.select((d) => d.ekKatmanlar[ki])); if (kli >= katman.klipler.length) return const SizedBox.shrink(); final k = katman.klipler[kli]; return AlertDialog( title: const Text('Ses Ayarı'), content: SizedBox( width: 320, child: Column(mainAxisSize: MainAxisSize.min, children: [ _SliderSatiri( etiket: 'Seviye', deger: k.seviye, min: 0, max: 2, gosterim: '${(k.seviye * 100).round()}%', onChanged: (v) => m.katmanKlipGuncelle(ki, kli, (x) => x.seviye = v), ), _SliderSatiri( etiket: 'Fade In', deger: k.fadeIn, min: 0, max: (k.sure / 2).clamp(0.5, 10), gosterim: '${k.fadeIn.toStringAsFixed(1)}s', onChanged: (v) => m.katmanSesFade(ki, kli, fadeIn: v), ), _SliderSatiri( etiket: 'Fade Out', deger: k.fadeOut, min: 0, max: (k.sure / 2).clamp(0.5, 10), gosterim: '${k.fadeOut.toStringAsFixed(1)}s', onChanged: (v) => m.katmanSesFade(ki, kli, fadeOut: v), ), ]), ), actions: [ FilledButton( onPressed: () => Navigator.pop(ctx), child: const Text('Tamam')), ], ); }), ); } /// Overlay video/resim: konum (x/y) + boyut + opaklık ayar paneli. void _overlayAyarDialog( BuildContext context, WidgetRef ref, DirektorMotor m) { showDialog( context: context, builder: (ctx) => Consumer(builder: (c, ref2, _) { final katman = ref2.watch(direktorProvider.select((d) => d.ekKatmanlar[ki])); if (kli >= katman.klipler.length) return const SizedBox.shrink(); final k = katman.klipler[kli]; return AlertDialog( title: const Text('Overlay Ayarı'), content: SizedBox( width: 320, child: Column(mainAxisSize: MainAxisSize.min, children: [ _SliderSatiri( etiket: 'Boyut', deger: k.olcekOran, min: 0.05, max: 1.0, gosterim: '${(k.olcekOran * 100).round()}%', onChanged: (v) => m.katmanKlipGuncelle(ki, kli, (x) => x.olcekOran = v), ), _SliderSatiri( etiket: 'Opaklık', deger: k.seviye, min: 0, max: 1, gosterim: '${(k.seviye * 100).round()}%', onChanged: (v) => m.katmanKlipGuncelle(ki, kli, (x) => x.seviye = v), ), _SliderSatiri( etiket: 'Yatay', deger: k.x, min: 0, max: 1, gosterim: '${(k.x * 100).round()}%', onChanged: (v) => m.katmanKlipGuncelle(ki, kli, (x) => x.x = v), ), _SliderSatiri( etiket: 'Dikey', deger: k.y, min: 0, max: 1, gosterim: '${(k.y * 100).round()}%', onChanged: (v) => m.katmanKlipGuncelle(ki, kli, (x) => x.y = v), ), ]), ), actions: [ FilledButton( onPressed: () => Navigator.pop(ctx), child: const Text('Tamam')), ], ); }), ); } void _yaziDuzenle(BuildContext context, DirektorMotor m) { final ctrl = TextEditingController(text: klip.yazi); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Yazıyı Düzenle'), content: TextField(controller: ctrl, autofocus: true), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Vazgeç')), FilledButton( onPressed: () { m.katmanKlipGuncelle(ki, kli, (k) => k.yazi = ctrl.text); Navigator.pop(ctx); }, child: const Text('Kaydet'), ), ], ), ); } } // ───────────────────────── FX şeridi — SÜRELİ efekt span'ları ───────────────────────── final seciliFxProvider = StateProvider((ref) => null); class _FxSeridi extends ConsumerWidget { const _FxSeridi({required this.olcek}); final double olcek; @override Widget build(BuildContext context, WidgetRef ref) { final spanlar = ref.watch(direktorProvider.select((d) => d.efektSpanlari)); final secili = ref.watch(seciliFxProvider); final m = ref.read(direktorProvider.notifier); // Kütüphaneden efekt sürükle → FX şeridine bırak = süreli span return DragTarget( onWillAcceptWithDetails: (d) => d.data is EfektTasima && (VideoEfekt.bul((d.data as EfektTasima).id)?.zamanli ?? false), onAcceptWithDetails: (d) { final id = (d.data as EfektTasima).id; final t = _fareZamani(d.offset, olcek).clamp(0.0, 100000.0); m.spanEkle(id, m.snapla(t, olcek)); ref.read(seciliFxProvider.notifier).state = ref.read(direktorProvider).efektSpanlari.length - 1; }, builder: (context, adaylar, _) => Stack(clipBehavior: Clip.none, children: [ Positioned.fill( child: Container( decoration: BoxDecoration( color: adaylar.isNotEmpty ? FikoRenkler.neonPembe.withValues(alpha: 0.14) : FikoRenkler.neonPembe.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(3), ), ), ), for (var i = 0; i < spanlar.length; i++) _FxSpan( indeks: i, span: spanlar[i], olcek: olcek, secili: secili == i), ]), ); } } class _FxSpan extends ConsumerWidget { const _FxSpan( {required this.indeks, required this.span, required this.olcek, required this.secili}); final int indeks; final EfektSpan span; final double olcek; final bool secili; @override Widget build(BuildContext context, WidgetRef ref) { final m = ref.read(direktorProvider.notifier); final etiket = VideoEfekt.bul(span.id)?.etiket ?? span.id; return Positioned( left: span.baslangic * olcek, top: 0, width: (span.sure * olcek).clamp(14.0, 200000.0), height: _fxY, child: GestureDetector( onTap: () => ref.read(seciliFxProvider.notifier).state = indeks, onDoubleTap: () { m.spanSil(indeks); ref.read(seciliFxProvider.notifier).state = null; }, onHorizontalDragUpdate: (d) { final t = _fareZamani(d.globalPosition, olcek) - span.sure / 2; m.spanGuncelle(indeks, baslangic: m.snapla(t.clamp(0, 100000), olcek)); }, child: Tooltip( message: '$etiket • ${span.sure.toStringAsFixed(1)}sn (çift tık: sil)', child: Stack(clipBehavior: Clip.none, children: [ Container( margin: const EdgeInsets.symmetric(horizontal: 1), decoration: BoxDecoration( gradient: LinearGradient(colors: [ FikoRenkler.neonPembe .withValues(alpha: secili ? 0.85 : 0.55), FikoRenkler.neonMor.withValues(alpha: secili ? 0.85 : 0.55), ]), borderRadius: BorderRadius.circular(3), border: secili ? Border.all(color: Colors.white, width: 1) : null, ), alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 4), child: Text('✨ $etiket', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 8, color: Colors.white)), ), // sağ kenar: süre boyutlandır Positioned( right: -2, top: 0, bottom: 0, width: 9, child: MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, child: GestureDetector( behavior: HitTestBehavior.translucent, onHorizontalDragUpdate: (d) { final ham = _fareZamani(d.globalPosition, olcek); final snap = m.snapla(ham, olcek); m.spanGuncelle(indeks, sure: snap - span.baslangic); }, child: const SizedBox(), ), ), ), ]), ), ), ); } } // ───────────────────────── Ana ses şeridi ───────────────────────── class _AnaSesSeridi extends ConsumerWidget { const _AnaSesSeridi( {required this.olcek, required this.toplam, required this.sarki}); final double olcek; final double toplam; final String sarki; @override Widget build(BuildContext context, WidgetRef ref) { final gen = (toplam * olcek).clamp(50.0, 200000.0); return SizedBox( width: gen, height: _sesAnaY, child: Container( decoration: BoxDecoration( color: FikoRenkler.neonCyan.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(5), border: Border.all(color: FikoRenkler.neonCyan.withValues(alpha: 0.3)), ), clipBehavior: Clip.antiAlias, child: sarki.isEmpty ? const Center( child: Text('ses yok', style: TextStyle(fontSize: 9, color: FikoRenkler.neonCyan))) : FutureBuilder( future: FfmpegServisi.dalgaFormu(sarki, renk: '00E5FF'), builder: (c, snap) => snap.data != null && File(snap.data!).existsSync() ? Image.file(File(snap.data!), fit: BoxFit.fill, gaplessPlayback: true) : const SizedBox(), ), ), ); } } class _Btn extends StatelessWidget { const _Btn(this.ikon, this.ipucu, this.onTap, {this.renk}); final IconData ikon; final String ipucu; final VoidCallback onTap; final Color? renk; @override Widget build(BuildContext context) => Tooltip( message: ipucu, child: InkWell( borderRadius: BorderRadius.circular(5), onTap: onTap, child: Padding( padding: const EdgeInsets.all(3.5), child: Icon(ikon, size: 15.5, color: renk)), ), ); } class _Tgl extends StatelessWidget { const _Tgl(this.ikon, this.ipucu, this.acik, this.onTap, {this.renk}); final IconData ikon; final String ipucu; final bool acik; final VoidCallback onTap; final Color? renk; @override Widget build(BuildContext context) { final r = renk ?? FikoRenkler.neonAmber; return Tooltip( message: ipucu, child: InkWell( borderRadius: BorderRadius.circular(5), onTap: onTap, child: Container( padding: const EdgeInsets.all(3.5), decoration: acik ? BoxDecoration( color: r.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(5)) : null, child: Icon(ikon, size: 15.5, color: acik ? r : null), ), ), ); } } class _MiniIkon extends StatelessWidget { const _MiniIkon(this.ikon, this.renk, this.onTap); final IconData ikon; final Color? renk; final VoidCallback onTap; @override Widget build(BuildContext context) => InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.all(1.5), child: Icon(ikon, size: 11, color: renk)), ); } class _SliderSatiri extends StatelessWidget { const _SliderSatiri({ required this.etiket, required this.deger, required this.min, required this.max, required this.gosterim, required this.onChanged, }); final String etiket; final double deger; final double min, max; final String gosterim; final ValueChanged onChanged; @override Widget build(BuildContext context) => Row(children: [ SizedBox( width: 64, child: Text(etiket, style: const TextStyle(fontSize: 12))), Expanded( child: Slider( value: deger.clamp(min, max), min: min, max: max, onChanged: onChanged, ), ), SizedBox( width: 42, child: Text(gosterim, textAlign: TextAlign.right, style: const TextStyle( fontSize: 11, color: FikoRenkler.neonCyan))), ]); } class _Ayir extends StatelessWidget { @override Widget build(BuildContext context) => Container( width: 1, height: 15, margin: const EdgeInsets.symmetric(horizontal: 4), color: Theme.of(context).dividerColor); } String _mmss(double s) { final dk = (s ~/ 60).toString().padLeft(2, '0'); final sn = (s % 60).toStringAsFixed(s < 60 ? 1 : 0).padLeft(s < 60 ? 4 : 2, '0'); return '$dk:$sn'; }