import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../cekirdek/kimlik.dart'; import '../../durum/proje_durumu.dart'; import '../../modeller/job.dart'; import '../../modeller/proje.dart'; import '../../modeller/segment.dart'; import '../../servisler/ai/ollama_prompt_zinciri.dart'; import '../../servisler/ffmpeg/ffmpeg_filtre.dart'; import '../../servisler/ffmpeg/ffmpeg_servis.dart'; import '../../servisler/job_orkestratoru.dart'; import '../../servisler/ltx/ltx_baglanti_servisi.dart'; import '../../servisler/segment_olusturucu.dart'; import '../../servisler/wangp/wangp_baslatici.dart'; import '../ortak/bolucu.dart'; import 'inspector_paneli.dart'; import 'onizleme_oynatici.dart'; import 'segment_listesi.dart'; import 'waveform_widget.dart'; /// İzleme modu: bir segment üretilince sıradakini otomatik kuyruğa al. final izlemeModuProvider = StateProvider((ref) => false); class GorunumStudio extends ConsumerWidget { const GorunumStudio({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final proje = ref.watch(projeProvider); final theme = Theme.of(context); if (proje == null) { return const Center(child: Text('Önce bir proje aç.')); } final solPanel = Container( decoration: BoxDecoration(color: theme.colorScheme.surface), child: const SegmentListesi(), ); final ortaPanel = Padding( padding: const EdgeInsets.all(12), child: Column( children: [ _ToplaUretBari(proje: proje), const SizedBox(height: 12), Expanded( child: DikeyBolucu( baslangicUstYukseklik: 360, minUst: 200, minAlt: 120, ust: const OnizlemeOynatici(), alt: const Padding( padding: EdgeInsets.only(top: 8), child: WaveformWidget(), ), ), ), ], ), ); final sagPanel = Container( decoration: BoxDecoration(color: theme.colorScheme.surface), child: const InspectorPaneli(), ); return YatayBolucu( baslangicSolGenislik: 240, minSol: 160, minSag: 320, sol: solPanel, sag: YatayBolucu( baslangicSolGenislik: 800, minSol: 300, minSag: 240, sol: ortaPanel, sag: sagPanel, ), ); } } class _ToplaUretBari extends ConsumerWidget { final Proje proje; const _ToplaUretBari({required this.proje}); @override Widget build(BuildContext context, WidgetRef ref) { final tamam = proje.segmentler.where((s) => s.durum == SegmentDurum.tamam).length; final toplam = proje.segmentler.length; final izleme = ref.watch(izlemeModuProvider); return Card( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ Icon(Icons.movie_creation_outlined, size: 18, color: Theme.of(context).colorScheme.primary), const SizedBox(width: 8), Text('$tamam / $toplam segment hazır', style: Theme.of(context).textTheme.bodyMedium), const Spacer(), Tooltip( message: izleme ? 'İzleme modu açık — biten segmentin sonrasını otomatik kuyruğa alır' : 'İzleme modu kapalı', child: FilterChip( avatar: Icon(izleme ? Icons.visibility : Icons.visibility_off, size: 14), label: const Text('İzleme', style: TextStyle(fontSize: 11)), selected: izleme, onSelected: (v) => ref.read(izlemeModuProvider.notifier).state = v, ), ), const SizedBox(width: 8), OutlinedButton.icon( icon: const Icon(Icons.analytics_outlined, size: 16), label: const Text('Enerji Analizi'), onPressed: () { JobOrkestratoru.ornek.ekle(Job( id: FikoYTId.yeni(), tip: JobTipi.analiziYap, projeId: proje.id, oncelik: 50, )); }, ), const SizedBox(width: 8), OutlinedButton.icon( icon: const Icon(Icons.auto_awesome, size: 16), label: const Text('AI Promptlar'), onPressed: () => _aiPromptUret(context, ref, proje), ), const SizedBox(width: 8), FilledButton.icon( icon: const Icon(Icons.play_arrow, size: 16), label: Text('Tümünü Üret ($toplam)'), onPressed: () async { if (!await _herhangiMotorAcikMi(context, ref)) return; // En güncel state'i diske yaz, sonra oku await ref.read(projeProvider.notifier).hemenKaydet(); final guncelProje = ref.read(projeProvider)!; final harita = await SegmentOlusturucu.sesParcalariniHazirla( guncelProje); // Sadece üretilmemiş veya hatalı segmentleri kuyruğa al // (uretiliyor ve kuyruk durumundakileri tekrarlama) final hedefler = guncelProje.segmentler .where((s) => s.durum == SegmentDurum.bos || s.durum == SegmentDurum.hata || s.durum == SegmentDurum.atlandi) .toList(); // Sıralı üretim için öncelik = 1000 - seg.sira // (S0 → 1000, S1 → 999, ...). Tie-breaker görevi görüyor, // küçük sira önce çalıştırılır → zincir görsel doğru bağlanır. for (final s in hedefler) { JobOrkestratoru.ornek.ekle(Job( id: FikoYTId.yeni(), tip: JobTipi.segmentUret, projeId: guncelProje.id, params: { 'segmentId': s.id, 'sesYolu': harita[s.id], 'girisGorseli': guncelProje.kaynak.girisGorseliPath, }, oncelik: 1000 - s.sira, )); } if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '${hedefler.length} segment kuyruğa eklendi', ), ), ); } }, ), const SizedBox(width: 4), TextButton.icon( icon: const Icon(Icons.auto_mode, size: 14), label: const Text('5 Seed', style: TextStyle(fontSize: 11)), onPressed: () => _seedBatch(context, ref, proje), ), const SizedBox(width: 8), OutlinedButton.icon( icon: const Icon(Icons.merge_type, size: 16), label: const Text('Birleştir'), onPressed: () => _birlestirBaslat(context, proje), ), const SizedBox(width: 4), TextButton.icon( icon: const Icon(Icons.preview, size: 14), label: const Text('Hızlı Ön İzleme', style: TextStyle(fontSize: 11)), onPressed: () => _hizliOnizleme(context, proje), ), ], ), ), ); } } /// Ollama ile segmentlere ardışık AI prompt'ları üretir. Future _aiPromptUret( BuildContext context, WidgetRef ref, Proje proje) async { showDialog( context: context, barrierDismissible: false, builder: (_) => const AlertDialog( content: Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2), ), SizedBox(width: 16), Flexible(child: Text("Ollama prompt'lar üretiyor...")), ], ), ), ), ); final ollama = OllamaPromptZinciri(); if (!await ollama.sagliklimi()) { if (context.mounted) Navigator.of(context, rootNavigator: true).pop(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Ollama 11434 portunda yok')), ); } return; } final segMap = proje.segmentler .map((s) => { 'sira': s.sira, 'sure': s.bit - s.basla, 'enerji': s.analizMeta?.enerjiSeviyesi ?? '', 'mevcut': s.prompt ?? '', 'basla': s.basla, 'bit': s.bit, }) .toList(); final prompts = await ollama.zincirOlustur( segmentler: segMap, tema: proje.global.varsayilanPrompt, dil: 'tr', stil: 'sinematik', ); if (context.mounted) Navigator.of(context, rootNavigator: true).pop(); int yazilan = 0; for (var i = 0; i < prompts.length && i < proje.segmentler.length; i++) { final yeniPrompt = prompts[i].trim(); if (yeniPrompt.isEmpty) continue; final segId = proje.segmentler[i].id; ref.read(projeProvider.notifier).guncelle((p) { final yeniler = p.segmentler.map((s) { if (s.id != segId) return s; return s.copyWith(prompt: yeniPrompt); }).toList(); return p.copyWith(segmentler: yeniler); }); yazilan++; } if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('$yazilan segment için AI prompt üretildi')), ); } } /// Seçili segment için 5 farklı seed ile job kuyruğa ekler. Future _seedBatch( BuildContext context, WidgetRef ref, Proje proje) async { final secId = ref.read(secilenSegmentIdProvider); if (secId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Önce segment seç')), ); return; } final seg = proje.segmentler.where((s) => s.id == secId).toList(); if (seg.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Seçili segment bulunamadı')), ); return; } final hedef = seg.first; await ref.read(projeProvider.notifier).hemenKaydet(); final guncelProje = ref.read(projeProvider)!; final harita = await SegmentOlusturucu.sesParcalariniHazirla(guncelProje); final taban = DateTime.now().millisecondsSinceEpoch; for (int k = 0; k < 5; k++) { final seed = taban + k * 1000; JobOrkestratoru.ornek.ekle(Job( id: FikoYTId.yeni(), tip: JobTipi.segmentUret, projeId: guncelProje.id, params: { 'segmentId': hedef.id, 'sesYolu': harita[hedef.id], 'girisGorseli': guncelProje.kaynak.girisGorseliPath, 'seedOverride': seed, }, )); } if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('5 varyant kuyruğa eklendi (rastgele seed)')), ); } } /// LTX veya WanGP'den herhangi biri açık mı? Değilse kullanıcıya bilgi ver. Future _herhangiMotorAcikMi(BuildContext context, WidgetRef ref) async { final ltx = ref.read(ltxBaglantiProvider); final wangpAcik = await WanGpBaslatici.calisiyorMu(); if (ltx.baglandi || wangpAcik) return true; if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Colors.red.shade700, content: const Text( 'Hiçbir motor açık değil. LTX-Desktop-WanGP-Baslat.bat veya ' 'WanGP-Standalone-Baslat.bat çalıştır.', style: TextStyle(color: Colors.white), ), action: SnackBarAction( label: 'AYARLAR', textColor: Colors.white, onPressed: () => context.go('/ayarlar'), ), duration: const Duration(seconds: 6), ), ); } return false; } // ─── Geçiş Seçici Dialog ───────────────────────────────────────────────────── class _BirlestirSonuc { final List gecisler; const _BirlestirSonuc({required this.gecisler}); } class _BirlestirDialog extends StatefulWidget { final int tamamlananSayisi; final String hedefKlasor; final String projIsim; const _BirlestirDialog({ required this.tamamlananSayisi, required this.hedefKlasor, required this.projIsim, }); @override State<_BirlestirDialog> createState() => _BirlestirDialogState(); } class _BirlestirDialogState extends State<_BirlestirDialog> { GecisTipi _globalTip = GecisTipi.kesim; double _globalSure = 0.5; @override Widget build(BuildContext context) { final theme = Theme.of(context); return AlertDialog( title: const Text('Birleştir'), content: SizedBox( width: 520, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${widget.tamamlananSayisi} tamamlanmış segment birleştirilecek.', ), const SizedBox(height: 16), Text('Segment geçiş efekti:', style: theme.textTheme.labelMedium ?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Wrap( spacing: 6, runSpacing: 6, children: GecisTipi.values.map((tip) { final secili = _globalTip == tip; return ChoiceChip( label: Text(tip.gorselIsim, style: const TextStyle(fontSize: 12)), selected: secili, onSelected: (_) => setState(() => _globalTip = tip), ); }).toList(), ), if (_globalTip != GecisTipi.kesim) ...[ const SizedBox(height: 16), Row( children: [ Text('Geçiş süresi: ', style: theme.textTheme.labelMedium ?.copyWith(fontWeight: FontWeight.bold)), Text('${_globalSure.toStringAsFixed(1)} sn', style: theme.textTheme.labelMedium ?.copyWith(color: theme.colorScheme.primary)), ], ), Slider( value: _globalSure, min: 0.2, max: 1.5, divisions: 13, label: '${_globalSure.toStringAsFixed(1)} sn', onChanged: (v) => setState(() => _globalSure = v), ), Text( '💡 Önerilen: 0.3–0.8 sn. Uzun süreler siyah flash yapabilir.', style: theme.textTheme.bodySmall ?.copyWith(color: theme.colorScheme.outline), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(8), ), child: const Text( '⚠️ Efektli birleştirme re-encode yapar — codec-copy\'dan yavaştır.\n' 'Tüm segmentler aynı çözünürlük ve FPS\'te olmalıdır.', style: TextStyle(fontSize: 11), ), ), ], const SizedBox(height: 12), const Divider(), const SizedBox(height: 4), Text('Çıktı klasörü:', style: theme.textTheme.labelSmall ?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 4), SelectableText( widget.hedefKlasor, style: const TextStyle(fontSize: 11, fontFamily: 'monospace'), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('İptal')), OutlinedButton.icon( icon: const Icon(Icons.folder_open, size: 14), label: const Text('Klasörü Aç'), onPressed: () async { await Directory(widget.hedefKlasor).create(recursive: true); if (Platform.isWindows) { await Process.run('cmd', ['/c', 'start', '', widget.hedefKlasor]); } }, ), FilledButton( onPressed: () { final toplamSinir = widget.tamamlananSayisi - 1; final gecisler = List.generate( toplamSinir, (i) => GecisAyari( boundaryIdx: i, tip: _globalTip, sureSn: _globalSure, ), ); Navigator.pop(context, _BirlestirSonuc(gecisler: gecisler)); }, child: Text(_globalTip == GecisTipi.kesim ? 'Birleştir (Kesim)' : 'Birleştir (${_globalTip.gorselIsim})'), ), ], ); } } /// Birleştirme job'unu kuyruğa ekler. Future _birlestirBaslat(BuildContext context, Proje proje) async { final tamamlanan = proje.segmentler.where((s) => s.videoYolu != null).length; if (tamamlanan == 0) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Birleştirilecek üretilmiş segment yok')), ); return; } final hedefKlasor = p.join(proje.klasorYolu, 'cikti'); final result = await showDialog<_BirlestirSonuc>( context: context, builder: (c) => _BirlestirDialog( tamamlananSayisi: tamamlanan, hedefKlasor: hedefKlasor, projIsim: proje.isim, ), ); if (result == null || !context.mounted) return; final aboneIptal = JobOrkestratoru.ornek.events.listen((j) async { if (j.tip != JobTipi.birlestir || j.projeId != proje.id) return; if (j.durum == JobDurum.tamam) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Birleştirme tamam: ${p.basename(j.fazAciklama)}'), duration: const Duration(seconds: 5), action: SnackBarAction( label: 'KLASÖRÜ AÇ', onPressed: () { if (Platform.isWindows) { Process.run('cmd', ['/c', 'start', '', hedefKlasor]); } }, ), ), ); } else if (j.durum == JobDurum.hata) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Colors.red.shade700, content: Text('Birleştirme hatası: ${j.hata}', style: const TextStyle(color: Colors.white)), ), ); } }); Future.delayed(const Duration(minutes: 30), () => aboneIptal.cancel()); await JobOrkestratoru.ornek.ekle(Job( id: FikoYTId.yeni(), tip: JobTipi.birlestir, projeId: proje.id, oncelik: 10, params: { 'gecisler': result.gecisler.map((g) => g.toJson()).toList(), }, )); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Birleştirme kuyruğa eklendi — bittiğinde bildirilecek'), duration: Duration(seconds: 2), ), ); } } /// Tamamlanmış segmentleri düşük çözünürlükte hızlı önizleme olarak birleştirir. Future _hizliOnizleme(BuildContext context, Proje proje) async { final yollar = proje.segmentler .where((s) => s.durum == SegmentDurum.tamam && s.videoYolu != null) .map((s) => s.videoYolu!) .toList(); if (yollar.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Üretilmiş segment yok')), ); return; } final ts = DateTime.now().millisecondsSinceEpoch; final hedef = '${proje.klasorYolu}/onizleme_$ts.mp4'; final ilerlemeKontrol = _OnizlemeIlerlemeKontrol(); showDialog( context: context, barrierDismissible: false, builder: (_) => _OnizlemeProgressDialog(kontrol: ilerlemeKontrol), ); try { await FfmpegServis.hizliOnizleme( videoYollari: yollar, hedefYol: hedef, hedefGenislik: 480, ilerleme: (yuzde, faz) => ilerlemeKontrol.guncelle(yuzde, faz), ); if (context.mounted) Navigator.of(context, rootNavigator: true).pop(); if (Platform.isWindows) { await Process.run('cmd', ['/c', 'start', '', hedef]); } else if (Platform.isMacOS) { await Process.run('open', [hedef]); } else { await Process.run('xdg-open', [hedef]); } } catch (e) { if (context.mounted) Navigator.of(context, rootNavigator: true).pop(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Hızlı önizleme başarısız: $e')), ); } } } class _OnizlemeIlerlemeKontrol { final ValueNotifier yuzde = ValueNotifier(0); final ValueNotifier faz = ValueNotifier('Başlatılıyor'); void guncelle(double y, String f) { yuzde.value = y; faz.value = f; } } class _OnizlemeProgressDialog extends StatelessWidget { final _OnizlemeIlerlemeKontrol kontrol; const _OnizlemeProgressDialog({required this.kontrol}); @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Hızlı Ön İzleme'), content: SizedBox( width: 360, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ValueListenableBuilder( valueListenable: kontrol.yuzde, builder: (_, y, __) => LinearProgressIndicator( value: y > 0 ? (y / 100).clamp(0.0, 1.0) : null, ), ), const SizedBox(height: 12), ValueListenableBuilder( valueListenable: kontrol.faz, builder: (_, f, __) => Text( f, style: Theme.of(context).textTheme.bodySmall, ), ), ], ), ), ); } }