import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/kabuk.dart'; import '../app/tema.dart'; import '../bilesenler/bilesenler.dart'; import '../comfy/comfy_istemci.dart'; import '../comfy/uretim_kuyrugu.dart'; import '../comfy/workflow/muzik_workflow.dart'; import '../comfy/workflow/resim_edit_workflow.dart'; import '../comfy/workflow/resim_workflow.dart'; import '../comfy/workflow/ses_klon_workflow.dart'; import '../comfy/workflow/video_workflow.dart'; import '../muzik/muzik_form_state.dart'; import '../resim/resim_form_state.dart'; import '../video/video_form_state.dart'; import '../veri/isar_servisi.dart'; import '../veri/modeller/uretim_kaydi.dart'; import 'profesyonel_calar.dart'; import 'ses_kes_dialog.dart'; import 'video_oynatici_dialog.dart'; final galeriFiltreProvider = StateProvider((ref) => null); /// Varyant grupları: kayıtları etkinGrup'a göre toplar; en yenisi temsilci. ({List temsilciler, Map> gruplar}) varyantGrupla(List kayitlar) { final gruplar = >{}; for (final k in kayitlar) { gruplar.putIfAbsent(k.etkinGrup, () => []).add(k); } final temsilciler = []; for (final liste in gruplar.values) { liste.sort((a, b) => b.olusturmaZamani.compareTo(a.olusturmaZamani)); temsilciler.add(liste.first); } temsilciler .sort((a, b) => b.olusturmaZamani.compareTo(a.olusturmaZamani)); return (temsilciler: temsilciler, gruplar: gruplar); } class GaleriEkrani extends ConsumerWidget { const GaleriEkrani({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final filtre = ref.watch(galeriFiltreProvider); final tumKayitlar = (ref.watch(uretimKayitlariProvider).valueOrNull ?? []) .where((k) => k.durum == UretimDurumu.tamamlandi && (filtre == null || k.tip == filtre)) .toList(); final grup = varyantGrupla(tumKayitlar); final kayitlar = grup.temsilciler; return Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.collections_rounded, color: FikoRenkler.neonCyan, size: 20), const SizedBox(width: 8), const Text('Galeri', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w800)), const SizedBox(width: 14), SegmentedButton( style: const ButtonStyle( visualDensity: VisualDensity.compact, textStyle: WidgetStatePropertyAll(TextStyle(fontSize: 12))), segments: const [ ButtonSegment(value: null, label: Text('Hepsi')), ButtonSegment( value: UretimTipi.muzik, label: Text('Müzik')), ButtonSegment( value: UretimTipi.resim, label: Text('Resim')), ButtonSegment( value: UretimTipi.video, label: Text('Video')), ], selected: {filtre}, onSelectionChanged: (s) => ref.read(galeriFiltreProvider.notifier).state = s.first, ), const Spacer(), Text('${kayitlar.length} grup • ${tumKayitlar.length} üretim', style: TextStyle( fontSize: 12, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.5))), ], ), const SizedBox(height: 14), Expanded( child: kayitlar.isEmpty ? const _BosGaleri() : GridView.builder( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 420, mainAxisExtent: 168, crossAxisSpacing: 12, mainAxisSpacing: 12, ), itemCount: kayitlar.length, itemBuilder: (_, i) => _GaleriKarti( kayit: kayitlar[i], varyantlar: grup.gruplar[kayitlar[i].etkinGrup] ?? const []), ), ), ], ), ); } } class _BosGaleri extends StatelessWidget { const _BosGaleri(); @override Widget build(BuildContext context) { final soluk = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4); return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.library_music_outlined, size: 64, color: soluk), const SizedBox(height: 12), Text('Henüz üretim yok', style: TextStyle(fontSize: 16, color: soluk)), const SizedBox(height: 6), Text('Müzik Stüdyosu\'ndan ilk üretiminizi başlatın', style: TextStyle(fontSize: 12.5, color: soluk)), ], ), ); } } class _GaleriKarti extends ConsumerWidget { const _GaleriKarti({required this.kayit, this.varyantlar = const []}); final UretimKaydi kayit; final List varyantlar; @override Widget build(BuildContext context, WidgetRef ref) { final soluk = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); final parametreler = _parametreOzeti(); return FikoKart( padding: const EdgeInsets.fromLTRB(12, 10, 12, 6), accent: switch (kayit.tip) { UretimTipi.muzik => FikoRenkler.neonCyan, UretimTipi.resim => FikoRenkler.neonPembe, UretimTipi.video => FikoRenkler.neonMor, }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text(kayit.baslik, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700)), ), if (varyantlar.length > 1) Tooltip( message: 'Varyantları gör', child: InkWell( borderRadius: BorderRadius.circular(6), onTap: () => _varyantGezgini(context, ref), child: Container( padding: const EdgeInsets.symmetric( horizontal: 7, vertical: 2), decoration: BoxDecoration( color: FikoRenkler.neonCyan.withValues(alpha: 0.16), borderRadius: BorderRadius.circular(6), ), child: Text('×${varyantlar.length}', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w800, color: FikoRenkler.neonCyan)), ), ), ), IconButton( tooltip: kayit.favori ? 'Favoriden çıkar' : 'Favorile', visualDensity: VisualDensity.compact, icon: Icon( kayit.favori ? Icons.star : Icons.star_border, size: 18, color: kayit.favori ? FikoRenkler.neonAmber : soluk, ), onPressed: () async { await isar.writeTxn(() async { kayit.favori = !kayit.favori; await isar.uretimKaydis.put(kayit); }); }, ), _KartMenusu(kayit: kayit), ], ), Text( '${_tarihFmt(kayit.olusturmaZamani)}' '${kayit.uretimSuresi == null ? '' : ' • ${kayit.uretimSuresi!.inSeconds}sn sürdü'}', style: TextStyle(fontSize: 10.5, color: soluk), ), const SizedBox(height: 4), if (parametreler.isNotEmpty) Wrap( spacing: 5, runSpacing: 4, children: [ for (final rozet in parametreler) Container( padding: const EdgeInsets.symmetric( horizontal: 7, vertical: 2), decoration: BoxDecoration( color: FikoRenkler.neonMor.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(6), ), child: Text(rozet, style: const TextStyle( fontSize: 10, color: FikoRenkler.neonMor)), ), ], ), const Spacer(), if (kayit.ciktiDosyalari.isNotEmpty) switch (kayit.tip) { UretimTipi.muzik => _VaryantCalar(dosyalar: kayit.ciktiDosyalari), UretimTipi.resim => _ResimOnizleme(dosyalar: kayit.ciktiDosyalari), UretimTipi.video => _VideoOnizleme(dosyalar: kayit.ciktiDosyalari), }, ], ), ); } /// Grubun tüm varyantlarını listeleyen gezgin (oynat/önizle/sil). void _varyantGezgini(BuildContext context, WidgetRef ref) { showDialog( context: context, builder: (ctx) => AlertDialog( title: Text('Varyantlar — ${kayit.baslik} (${varyantlar.length})'), content: SizedBox( width: 560, height: 420, child: ListView.separated( itemCount: varyantlar.length, separatorBuilder: (_, _) => const SizedBox(height: 8), itemBuilder: (_, i) { final v = varyantlar[i]; final dosya = v.ciktiDosyalari.isNotEmpty ? v.ciktiDosyalari.first : ''; return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(ctx).colorScheme.surface, borderRadius: BorderRadius.circular(9), border: Border.all(color: Theme.of(ctx).dividerColor), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Text('#${varyantlar.length - i}', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w800, color: FikoRenkler.neonCyan)), const SizedBox(width: 8), Expanded( child: Text( '${v.baslik} • ${_tarihFmt(v.olusturmaZamani)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11.5)), ), if (v.tip == UretimTipi.video && dosya.isNotEmpty) IconButton( visualDensity: VisualDensity.compact, icon: const Icon(Icons.play_arrow, size: 17), onPressed: () => Process.start( 'cmd', ['/c', 'start', '', dosya], mode: ProcessStartMode.detached), ), IconButton( visualDensity: VisualDensity.compact, icon: const Icon(Icons.delete_outline, size: 16), onPressed: () async { await kayitVeDosyalariSil(v.id); if (ctx.mounted) Navigator.pop(ctx); }, ), ]), if (dosya.isNotEmpty) ...[ const SizedBox(height: 6), switch (v.tip) { UretimTipi.muzik => ProfesyonelCalar(dosyaYolu: dosya, kompakt: true), UretimTipi.resim => File(dosya).existsSync() ? ClipRRect( borderRadius: BorderRadius.circular(7), child: Image.file(File(dosya), height: 110, width: double.infinity, fit: BoxFit.cover, cacheWidth: 520)) : const SizedBox.shrink(), UretimTipi.video => const SizedBox.shrink(), }, ], ], ), ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Kapat')), ], ), ); } List _parametreOzeti() { try { final j = Map.from( jsonDecode(kayit.parametrelerJson) as Map); return [ if (j['bpm'] != null) '${j['bpm']} bpm', if (j['sureSn'] != null) '${(j['sureSn'] as num).round()} sn', if (j['tonAnahtari'] != null) j['tonAnahtari'] as String, if (j['seed'] != null) 'seed ${j['seed']}', ]; } catch (_) { return []; } } static String _tarihFmt(DateTime t) => '${t.day.toString().padLeft(2, '0')}.${t.month.toString().padLeft(2, '0')}.${t.year} ' '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}'; } /// Resim üretimi önizleme — küçük resim şeridi, tıklayınca tam ekran. class _ResimOnizleme extends StatelessWidget { const _ResimOnizleme({required this.dosyalar}); final List dosyalar; @override Widget build(BuildContext context) { return SizedBox( height: 56, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: dosyalar.length, separatorBuilder: (_, _) => const SizedBox(width: 5), itemBuilder: (_, i) { final dosya = File(dosyalar[i]); if (!dosya.existsSync()) return const SizedBox.shrink(); return GestureDetector( onTap: () => _tamEkran(context, dosyalar[i]), child: ClipRRect( borderRadius: BorderRadius.circular(7), child: Image.file(dosya, width: 56, height: 56, fit: BoxFit.cover, cacheWidth: 120), ), ); }, ), ); } void _tamEkran(BuildContext context, String yol) { showDialog( context: context, builder: (ctx) => GestureDetector( onTap: () => Navigator.pop(ctx), child: Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(24), child: InteractiveViewer( child: Image.file(File(yol), fit: BoxFit.contain), ), ), ), ); } } /// Video önizleme — film şeridi + harici oynatıcıda aç. class _VideoOnizleme extends StatelessWidget { const _VideoOnizleme({required this.dosyalar}); final List dosyalar; @override Widget build(BuildContext context) { final dosya = dosyalar.first; final varMi = File(dosya).existsSync(); return Row( children: [ Container( width: 56, height: 40, decoration: BoxDecoration( color: FikoRenkler.neonMor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(7), border: Border.all( color: FikoRenkler.neonMor.withValues(alpha: 0.4)), ), child: const Icon(Icons.movie_rounded, color: FikoRenkler.neonMor, size: 22), ), const SizedBox(width: 10), Expanded( child: FilledButton.icon( style: FilledButton.styleFrom( visualDensity: VisualDensity.compact, backgroundColor: FikoRenkler.neonMor.withValues(alpha: 0.85), ), icon: const Icon(Icons.play_arrow, size: 18), label: const Text('Oynat', style: TextStyle(fontSize: 12)), onPressed: varMi ? () => showVideoOynatici(context, dosya, baslik: dosya.split(Platform.pathSeparator).last) : null, ), ), ], ); } } /// Çoklu varyant oynatıcı — birden çok seed çıktısı arasında geçiş. class _VaryantCalar extends StatefulWidget { const _VaryantCalar({required this.dosyalar}); final List dosyalar; @override State<_VaryantCalar> createState() => _VaryantCalarState(); } class _VaryantCalarState extends State<_VaryantCalar> { int _secili = 0; @override Widget build(BuildContext context) { final cok = widget.dosyalar.length > 1; final indeks = _secili.clamp(0, widget.dosyalar.length - 1); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (cok) Padding( padding: const EdgeInsets.only(bottom: 4), child: Wrap( spacing: 4, children: [ for (var i = 0; i < widget.dosyalar.length; i++) GestureDetector( onTap: () => setState(() => _secili = i), child: Container( padding: const EdgeInsets.symmetric( horizontal: 7, vertical: 2), decoration: BoxDecoration( color: i == indeks ? FikoRenkler.neonCyan.withValues(alpha: 0.22) : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(6), border: Border.all( color: i == indeks ? FikoRenkler.neonCyan : Theme.of(context).dividerColor), ), child: Text('V${i + 1}', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: i == indeks ? FikoRenkler.neonCyan : Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.6))), ), ), ], ), ), ProfesyonelCalar( dosyaYolu: widget.dosyalar[indeks], kompakt: true), ], ); } } class _KartMenusu extends ConsumerWidget { const _KartMenusu({required this.kayit}); final UretimKaydi kayit; @override Widget build(BuildContext context, WidgetRef ref) { return MenuAnchor( builder: (context, ctrl, _) => IconButton( visualDensity: VisualDensity.compact, icon: const Icon(Icons.more_vert, size: 18), onPressed: () => ctrl.isOpen ? ctrl.close() : ctrl.open(), ), menuChildren: [ MenuItemButton( leadingIcon: const Icon(Icons.refresh, size: 16), onPressed: () => ref.read(uretimKuyruguProvider.notifier).yenidenDene(kayit), child: const Text('Aynı parametrelerle yeniden üret'), ), MenuItemButton( leadingIcon: const Icon(Icons.shuffle, size: 16), onPressed: () => _varyasyonUret(ref), child: const Text('Varyasyon üret (yeni seed)'), ), if (kayit.tip == UretimTipi.muzik && kayit.ciktiDosyalari.isNotEmpty) ...[ MenuItemButton( leadingIcon: const Icon(Icons.content_cut, size: 16), onPressed: () => showSesKesDialog(context, ref, kayit), child: const Text('Kes / Böl'), ), MenuItemButton( leadingIcon: const Icon(Icons.splitscreen, size: 16), onPressed: () => _stemAyir(ref), child: const Text('Stem Ayır (vokal/davul/bas)'), ), ], if (kayit.tip == UretimTipi.resim && kayit.ciktiDosyalari.isNotEmpty) ...[ SubmenuButton( leadingIcon: const Icon(Icons.palette, size: 16), menuChildren: [ for (final (id, etiket) in colorGradePresetleri) MenuItemButton( onPressed: () => _resimDuzenle( ref, ResimEditTuru.renk, colorPreset: id), child: Text(etiket), ), ], child: const Text('Renk düzenle'), ), MenuItemButton( leadingIcon: const Icon(Icons.zoom_out_map, size: 16), onPressed: () => _resimDuzenle(ref, ResimEditTuru.buyut2x), child: const Text('Büyüt 2x'), ), MenuItemButton( leadingIcon: const Icon(Icons.zoom_out_map, size: 16), onPressed: () => _resimDuzenle(ref, ResimEditTuru.buyut4x), child: const Text('Büyüt 4x'), ), ], MenuItemButton( leadingIcon: const Icon(Icons.edit, size: 16), onPressed: () { switch (kayit.tip) { case UretimTipi.resim: ref.read(resimFormProvider.notifier).parametrelerdenYukle( kayit.parametrelerJson, kayit.sihirbazJson); ref.read(seciliEkranProvider.notifier).state = 1; case UretimTipi.video: ref .read(videoFormProvider.notifier) .parametrelerdenYukle(kayit.parametrelerJson); ref.read(seciliEkranProvider.notifier).state = 2; case UretimTipi.muzik: ref.read(muzikFormProvider.notifier).parametrelerdenYukle( kayit.parametrelerJson, kayit.sihirbazJson); ref.read(seciliEkranProvider.notifier).state = 0; } }, child: Text(kayit.tip == UretimTipi.resim ? 'Parametreleri forma yükle' : 'Parametre + sihirbazı forma yükle'), ), const Divider(), MenuItemButton( leadingIcon: const Icon(Icons.edit, size: 16), onPressed: () => _adlandir(context), child: const Text('Başlığı değiştir'), ), MenuItemButton( leadingIcon: const Icon(Icons.delete_outline, size: 16), onPressed: () async { await kayitVeDosyalariSil(kayit.id); }, child: const Text('Kaydı sil'), ), ], ); } void _adlandir(BuildContext context) { final ctrl = TextEditingController(text: kayit.baslik); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Başlığı Değiştir'), content: TextField( controller: ctrl, autofocus: true, decoration: const InputDecoration(labelText: 'Başlık')), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Vazgeç')), FilledButton( onPressed: () { if (ctrl.text.trim().isNotEmpty) { kayit.baslik = ctrl.text.trim(); isar.writeTxn(() => isar.uretimKaydis.put(kayit)); } Navigator.pop(ctx); }, child: const Text('Kaydet'), ), ], ), ); } /// Şarkıyı Demucs ile stem'lere ayırır (vokal/davul/bas/diğer) → varyant. Future _stemAyir(WidgetRef ref) async { try { final ad = await ref .read(comfyIstemciProvider) .resimYukle(kayit.ciktiDosyalari.first); await ref.read(uretimKuyruguProvider.notifier).uretimEkle( tip: UretimTipi.muzik, baslik: '${kayit.baslik} (stem ayırma)', parametrelerJson: kayit.parametrelerJson, workflow: demucsWorkflowOlustur(ad), varyantGrubu: kayit.etkinGrup, ); } catch (_) {} } /// Resmi ComfyUI'ye yükleyip düzenleme uygular, sonucu galeriye ekler. Future _resimDuzenle(WidgetRef ref, ResimEditTuru tur, {String colorPreset = 'cinematic'}) async { try { final ad = await ref .read(comfyIstemciProvider) .resimYukle(kayit.ciktiDosyalari.first); final etiket = switch (tur) { ResimEditTuru.renk => 'renk:$colorPreset', ResimEditTuru.buyut2x => '2x büyütme', ResimEditTuru.buyut4x => '4x büyütme', }; await ref.read(uretimKuyruguProvider.notifier).uretimEkle( tip: UretimTipi.resim, baslik: '${kayit.baslik} ($etiket)', parametrelerJson: kayit.parametrelerJson, workflow: resimEditWorkflowOlustur(ad, tur: tur, colorPreset: colorPreset), varyantGrubu: kayit.etkinGrup, ); } catch (_) {/* kuyruk hatayı gösterir */} } Future _varyasyonUret(WidgetRef ref) async { if (kayit.tip == UretimTipi.video) { final p = VideoParametreleri.fromJson(Map.from( jsonDecode(kayit.parametrelerJson) as Map)); final yeni = p.kopyala(seed: VideoParametreleri.rastgeleSeed()); await ref.read(uretimKuyruguProvider.notifier).uretimEkle( tip: kayit.tip, baslik: '${kayit.baslik} (varyasyon)', parametrelerJson: yeni.jsonString(), workflow: videoWorkflowOlustur(yeni), varyantGrubu: kayit.etkinGrup, ); return; } if (kayit.tip == UretimTipi.resim) { final p = ResimParametreleri.fromJson(Map.from( jsonDecode(kayit.parametrelerJson) as Map)); final yeni = p.kopyala(seed: ResimParametreleri.rastgeleSeed()); await ref.read(uretimKuyruguProvider.notifier).uretimEkle( tip: kayit.tip, baslik: '${kayit.baslik} (varyasyon)', parametrelerJson: yeni.jsonString(), sihirbazJson: kayit.sihirbazJson, workflow: resimWorkflowOlustur(yeni), varyantGrubu: kayit.etkinGrup, ); return; } final p = MuzikParametreleri.fromJson(Map.from( jsonDecode(kayit.parametrelerJson) as Map)); final yeni = p.kopyala(seed: MuzikParametreleri.rastgeleSeed()); await ref.read(uretimKuyruguProvider.notifier).uretimEkle( tip: kayit.tip, baslik: '${kayit.baslik} (varyasyon)', parametrelerJson: yeni.jsonString(), sihirbazJson: kayit.sihirbazJson, workflow: muzikWorkflowOlustur(yeni), varyantGrubu: kayit.etkinGrup, ); } }