import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/tema.dart'; import '../comfy/comfy_istemci.dart'; import '../comfy/kuyruk_paneli.dart'; import '../galeri/studyo_gecmis.dart'; import '../veri/isar_servisi.dart'; import '../veri/modeller/uretim_kaydi.dart'; import 'msr_form_state.dart'; import 'msr_servisi.dart'; final _gecmisAcikProvider = StateProvider((ref) => false); /// secim_model parametreleri için ComfyUI combo seçenekleri (node+field). final _modelSecenekleriProvider = FutureProvider.family, String>( (ref, anahtar) async { final parca = anahtar.split('::'); if (parca.length != 2) return const []; return ref .read(comfyIstemciProvider) .comboSecenekleri(parca[0], parca[1]); }, ); class MsrEkrani extends ConsumerWidget { const MsrEkrani({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final workflowlar = ref.watch(msrWorkflowlariProvider); return Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const _UstBar(), const SizedBox(height: 10), Expanded( child: workflowlar.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => _HataDurumu(mesaj: e.toString()), data: (liste) => liste.isEmpty ? const _BosDurum() : _Govde(workflowlar: liste), ), ), const SizedBox(height: 12), const KuyrukPaneli(tipFiltresi: UretimTipi.video), ], ), ); } } class _UstBar extends ConsumerWidget { const _UstBar(); @override Widget build(BuildContext context, WidgetRef ref) { return Row( children: [ const Icon(Icons.theaters_rounded, color: FikoRenkler.neonAmber, size: 20), const SizedBox(width: 8), const Text('Sahne / MSR Stüdyo', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w800)), const SizedBox(width: 10), Text('LTX 2.3 çoklu-özne video', style: TextStyle( fontSize: 11, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.5))), const Spacer(), IconButton( tooltip: 'Listeyi yenile', icon: const Icon(Icons.refresh, size: 18), onPressed: () => ref.invalidate(msrWorkflowlariProvider), ), IconButton( tooltip: 'Üretim geçmişi', icon: Icon(Icons.history, color: ref.watch(_gecmisAcikProvider) ? FikoRenkler.neonAmber : null), onPressed: () => ref.read(_gecmisAcikProvider.notifier).update((v) => !v), ), ], ); } } class _Govde extends ConsumerWidget { const _Govde({required this.workflowlar}); final List workflowlar; @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(msrFormProvider); // Seçili workflow yoksa ilkini seç. final secili = workflowlar.firstWhere( (w) => w.dosya == form.seciliDosya, orElse: () => workflowlar.first, ); if (form.seciliDosya.isEmpty) { WidgetsBinding.instance.addPostFrameCallback( (_) => ref.read(msrFormProvider.notifier).workflowSec(secili)); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _WorkflowSecici(workflowlar: workflowlar, secili: secili), const SizedBox(height: 8), if (secili.aciklama.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 6), child: Text(secili.aciklama, style: TextStyle( fontSize: 11.5, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.6))), ), const Divider(height: 14), Expanded( child: _DinamikForm(workflow: secili), ), const SizedBox(height: 10), if (form.hata != null) _HataSatiri(mesaj: form.hata!), _UretButonu(workflow: secili), ], ), ), ), ), if (ref.watch(_gecmisAcikProvider)) ...[ const SizedBox(width: 12), const StudyoGecmisPaneli(tip: UretimTipi.video), ], ], ); } } class _WorkflowSecici extends ConsumerWidget { const _WorkflowSecici({required this.workflowlar, required this.secili}); final List workflowlar; final MsrWorkflow secili; @override Widget build(BuildContext context, WidgetRef ref) { return SizedBox( height: 34, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: workflowlar.length, separatorBuilder: (_, _) => const SizedBox(width: 6), itemBuilder: (_, i) { final w = workflowlar[i]; final s = w.dosya == secili.dosya; return GestureDetector( onTap: () => ref.read(msrFormProvider.notifier).workflowSec(w), child: AnimatedContainer( duration: const Duration(milliseconds: 140), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: s ? FikoRenkler.neonAmber.withValues(alpha: 0.18) : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(9), border: Border.all( color: s ? FikoRenkler.neonAmber : Theme.of(context).dividerColor, width: s ? 1.3 : 1), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.movie_filter, size: 14, color: s ? FikoRenkler.neonAmber : null), const SizedBox(width: 6), Text(w.ad, style: TextStyle( fontSize: 12, fontWeight: s ? FontWeight.w700 : FontWeight.w500, color: s ? FikoRenkler.neonAmber : null)), ]), ), ); }, ), ); } } /// Workflow parametrelerine göre üretilen dinamik form (basit + gelişmiş). class _DinamikForm extends ConsumerWidget { const _DinamikForm({required this.workflow}); final MsrWorkflow workflow; @override Widget build(BuildContext context, WidgetRef ref) { final basit = workflow.parametreler.where((p) => !p.gelismis).toList(); final gelismis = workflow.parametreler.where((p) => p.gelismis).toList(); return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (final p in basit) Padding( padding: const EdgeInsets.only(bottom: 10), child: _ParametreAlani(par: p), ), if (gelismis.isNotEmpty) Theme( data: Theme.of(context) .copyWith(dividerColor: Colors.transparent), child: ExpansionTile( tilePadding: EdgeInsets.zero, childrenPadding: const EdgeInsets.only(top: 4), leading: const Icon(Icons.tune, size: 17, color: FikoRenkler.neonAmber), title: const Text('Gelişmiş', style: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w700, color: FikoRenkler.neonAmber)), children: [ for (final p in gelismis) Padding( padding: const EdgeInsets.only(bottom: 10), child: _ParametreAlani(par: p), ), ], ), ), ], ), ); } } /// Tek parametreyi tipine göre uygun widget'a yönlendirir. class _ParametreAlani extends ConsumerWidget { const _ParametreAlani({required this.par}); final MsrParametre par; @override Widget build(BuildContext context, WidgetRef ref) { switch (par.tip) { case MsrTip.metin: return _MetinAlani(par: par); case MsrTip.sayi: return _SayiAlani(par: par); case MsrTip.secim: return _SecimAlani(par: par, secenekler: par.secenekler); case MsrTip.secimModel: return _ModelAlani(par: par); case MsrTip.gorsel: return _DosyaSlot(par: par, gorsel: true); case MsrTip.sesDosya: return _DosyaSlot(par: par, gorsel: false); case MsrTip.bilinmiyor: return const SizedBox.shrink(); } } } class _MetinAlani extends ConsumerWidget { const _MetinAlani({required this.par}); final MsrParametre par; @override Widget build(BuildContext context, WidgetRef ref) { final deger = ref.watch(msrFormProvider.select((s) => s.degerler[par.key])); final metin = (deger ?? par.varsayilan ?? '').toString(); return TextFormField( key: ValueKey('msr_${par.key}_${metin.hashCode}'), initialValue: metin, maxLines: par.satir ? 4 : 1, style: const TextStyle(fontSize: 12.5), decoration: InputDecoration( labelText: par.label, alignLabelWithHint: par.satir, ), onChanged: (v) => ref.read(msrFormProvider.notifier).degerAyarla(par.key, v), ); } } class _SecimAlani extends ConsumerWidget { const _SecimAlani({required this.par, required this.secenekler}); final MsrParametre par; final List secenekler; @override Widget build(BuildContext context, WidgetRef ref) { final deger = ref.watch(msrFormProvider.select((s) => s.degerler[par.key])); final mevcut = (deger ?? par.varsayilan)?.toString(); final liste = secenekler.isEmpty ? (mevcut == null ? const [] : [mevcut]) : secenekler; final secili = liste.contains(mevcut) ? mevcut : (liste.isEmpty ? null : liste.first); return DropdownButtonFormField( initialValue: secili, isExpanded: true, decoration: InputDecoration(labelText: par.label), items: [ for (final s in liste) DropdownMenuItem( value: s, child: Text(s, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11.5))), ], onChanged: (v) { if (v != null) { ref.read(msrFormProvider.notifier).degerAyarla(par.key, v); } }, ); } } class _ModelAlani extends ConsumerWidget { const _ModelAlani({required this.par}); final MsrParametre par; @override Widget build(BuildContext context, WidgetRef ref) { final anahtar = '${par.node ?? ''}::${par.field ?? ''}'; final secenekler = ref.watch(_modelSecenekleriProvider(anahtar)); return secenekler.when( loading: () => InputDecorator( decoration: InputDecoration(labelText: par.label), child: const SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2)), ), error: (_, _) => _SecimAlani(par: par, secenekler: const []), data: (liste) => _SecimAlani(par: par, secenekler: liste), ); } } class _SayiAlani extends ConsumerWidget { const _SayiAlani({required this.par}); final MsrParametre par; @override Widget build(BuildContext context, WidgetRef ref) { final ham = ref.watch(msrFormProvider.select((s) => s.degerler[par.key])); final deger = ((ham ?? par.varsayilan) as num?)?.toDouble() ?? (par.min?.toDouble() ?? 0); final min = par.min?.toDouble() ?? 0; final max = par.max?.toDouble() ?? 100; final adim = par.adim?.toDouble() ?? 1; final tamSayi = adim == adim.roundToDouble() && adim >= 1; final bolum = adim > 0 && max > min ? ((max - min) / adim).round().clamp(1, 1000) : 100; void ayarla(double v) { final yuvarli = tamSayi ? v.round() : v; ref.read(msrFormProvider.notifier).degerAyarla(par.key, yuvarli); } return Row( children: [ SizedBox( width: 96, child: Text(par.label, style: const TextStyle(fontSize: 12), maxLines: 2, overflow: TextOverflow.ellipsis)), Expanded( child: Slider( value: deger.clamp(min, max), min: min, max: max, divisions: bolum, onChanged: ayarla, ), ), SizedBox( width: 52, child: Text( tamSayi ? deger.round().toString() : deger.toStringAsFixed(2), textAlign: TextAlign.right, style: const TextStyle(fontSize: 11.5)), ), ], ); } } /// Görsel/ses dosyası seçme + yükleme slotu (thumbnail/dosya adı). class _DosyaSlot extends ConsumerWidget { const _DosyaSlot({required this.par, required this.gorsel}); final MsrParametre par; final bool gorsel; @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(msrFormProvider); final n = ref.read(msrFormProvider.notifier); final yuklenenAd = (form.degerler[par.key] ?? '').toString(); final yerel = form.yuklenenler[par.key] ?? ''; final dolu = yuklenenAd.trim().isNotEmpty; final url = ref.watch(ayarlarProvider.select((a) => a.comfyUrl)); Future sec() async { final secim = await FilePicker.platform.pickFiles( type: gorsel ? FileType.image : FileType.audio, dialogTitle: '${par.label} seç', ); final yol = secim?.files.firstOrNull?.path; if (yol != null && File(yol).existsSync()) { await n.dosyaYukle(par.key, yol); } } return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (gorsel) GestureDetector( onTap: sec, child: Container( width: 64, height: 64, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: dolu ? FikoRenkler.neonCyan.withValues(alpha: 0.6) : Theme.of(context).dividerColor), ), clipBehavior: Clip.antiAlias, child: dolu ? Image.network( '$url/view?filename=${Uri.encodeComponent(yuklenenAd)}&type=input', fit: BoxFit.cover, errorBuilder: (_, _, _) => const Icon(Icons.image, size: 22), ) : Icon(Icons.add_photo_alternate, size: 22, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.4)), ), ) else Icon(Icons.audio_file, size: 28, color: dolu ? FikoRenkler.neonAmber : null), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(par.label, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600)), const SizedBox(height: 2), Text( dolu ? (yerel.isNotEmpty ? File(yerel).uri.pathSegments.last : yuklenenAd) : (gorsel ? 'Görsel seçilmedi' : 'Ses seçilmedi'), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 10.5, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.55))), ], ), ), TextButton.icon( style: TextButton.styleFrom(visualDensity: VisualDensity.compact), icon: Icon(dolu ? Icons.swap_horiz : Icons.upload, size: 15), label: Text(dolu ? 'Değiştir' : 'Seç', style: const TextStyle(fontSize: 11)), onPressed: sec, ), if (dolu) IconButton( visualDensity: VisualDensity.compact, icon: const Icon(Icons.close, size: 16), tooltip: 'Kaldır', onPressed: () => n.dosyaKaldir(par.key), ), ], ); } } class _UretButonu extends ConsumerWidget { const _UretButonu({required this.workflow}); final MsrWorkflow workflow; @override Widget build(BuildContext context, WidgetRef ref) { final gonderiliyor = ref.watch(msrFormProvider.select((s) => s.gonderiliyor)); return DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: const LinearGradient( colors: [FikoRenkler.neonAmber, FikoRenkler.neonPembe]), boxShadow: [ BoxShadow( color: FikoRenkler.neonAmber.withValues(alpha: 0.32), blurRadius: 14, offset: const Offset(0, 3)), ], ), child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: Colors.transparent, shadowColor: Colors.transparent, padding: const EdgeInsets.symmetric(vertical: 13), ), icon: gonderiliyor ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) : const Icon(Icons.bolt_rounded, size: 19), label: Text(gonderiliyor ? 'Gönderiliyor…' : '⚡ Üret', style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 13.5)), onPressed: gonderiliyor ? null : () async { final ok = await ref.read(msrFormProvider.notifier).uret(workflow); if (ok && context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Sahne üretimi kuyruğa eklendi'), duration: Duration(seconds: 2), )); } }, ), ); } } class _HataSatiri extends StatelessWidget { const _HataSatiri({required this.mesaj}); final String mesaj; @override Widget build(BuildContext context) { final renk = Theme.of(context).colorScheme.error; return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: renk.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), border: Border.all(color: renk.withValues(alpha: 0.4)), ), child: Row( children: [ Icon(Icons.error_outline, size: 16, color: renk), const SizedBox(width: 8), Expanded( child: Text(mesaj, style: TextStyle(fontSize: 11.5, color: renk)), ), ], ), ); } } class _BosDurum extends StatelessWidget { const _BosDurum(); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.theaters_outlined, size: 64, color: FikoRenkler.neonAmber.withValues(alpha: 0.5)), const SizedBox(height: 12), const Text('MSR workflow bulunamadı', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700)), const SizedBox(height: 6), Text('ComfyUI FikoYT MSR eklentisinin yüklü olduğundan emin olun', style: TextStyle( fontSize: 12, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.5))), ], ), ); } } class _HataDurumu extends ConsumerWidget { const _HataDurumu({required this.mesaj}); final String mesaj; @override Widget build(BuildContext context, WidgetRef ref) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.cloud_off, size: 60, color: Theme.of(context).colorScheme.error), const SizedBox(height: 12), const Text('MSR motoruna ulaşılamadı', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700)), const SizedBox(height: 6), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Text( 'ComfyUI ve FikoYT MSR eklentisi açık mı kontrol edin.\n$mesaj', textAlign: TextAlign.center, style: TextStyle( fontSize: 11.5, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.5))), ), const SizedBox(height: 14), FilledButton.icon( icon: const Icon(Icons.refresh, size: 16), label: const Text('Tekrar dene'), onPressed: () => ref.invalidate(msrWorkflowlariProvider), ), ], ), ); } }