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 '../bilesenler/bilesenler.dart'; import '../cekirdek/ffmpeg_servisi.dart'; import '../model/model_servisi.dart' show comfySamplerlarProvider, comfySchedulerlarProvider, birlesikListe; import '../comfy/kuyruk_paneli.dart'; import '../comfy/workflow/video_workflow.dart'; import '../galeri/studyo_gecmis.dart'; import '../lora/lora_servisi.dart'; import '../veri/modeller/lora_kaydi.dart'; import '../resim/nsfw_kataloglari.dart'; import '../resim/resim_kataloglari.dart'; import '../veri/isar_servisi.dart'; import '../veri/modeller/uretim_kaydi.dart'; import 'video_form_state.dart'; import 'video_kataloglari.dart'; import 'video_sihirbazi.dart'; final videoGecmisAcikProvider = StateProvider((ref) => false); class VideoEkrani extends ConsumerWidget { const VideoEkrani({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(videoFormProvider); return Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const _UstBar(), const SizedBox(height: 10), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 3, child: Card( child: Padding( padding: const EdgeInsets.all(10), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const _HizliStilSatiri(), const SizedBox(height: 8), const _PromptAlani(), const SizedBox(height: 8), _CipBolum('Kamera Hareketi', Icons.videocam, FikoRenkler.neonCyan, VideoKataloglari.kameraHareketleri), _CipBolum('Hareket Yoğunluğu', Icons.speed, FikoRenkler.neonPembe, VideoKataloglari.hareketYogunlugu), _CipBolum('Atmosfer', Icons.movie_filter, FikoRenkler.neonMor, VideoKataloglari.atmosfer), _CipBolum('Işık & Zaman', Icons.wb_twilight, FikoRenkler.neonAmber, VideoKataloglari.isikZaman), if (form.nsfwAcik) const _NsfwVideoBolum(), const Divider(height: 18), const _Parametreler(), if (form.gelismisGorunum) ...[ const SizedBox(height: 8), const _GelismisParametreler(), ], ], ), ), ), ), ), const SizedBox(width: 12), Expanded( flex: 2, child: Card( child: Padding( padding: const EdgeInsets.all(12), child: const SingleChildScrollView(child: _VideoYanPanel()), ), ), ), if (ref.watch(videoGecmisAcikProvider)) ...[ const SizedBox(width: 12), const StudyoGecmisPaneli(tip: UretimTipi.video), ], ], ), ), const SizedBox(height: 12), const KuyrukPaneli(tipFiltresi: UretimTipi.video), ], ), ); } } class _UstBar extends ConsumerWidget { const _UstBar(); @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(videoFormProvider); final notifier = ref.read(videoFormProvider.notifier); return Row( children: [ const Icon(Icons.movie_rounded, color: FikoRenkler.neonMor, size: 20), const SizedBox(width: 8), const Text('Video Stüdyosu', 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: false, label: Text('Basit')), ButtonSegment(value: true, label: Text('Gelişmiş')), ], selected: {form.gelismisGorunum}, onSelectionChanged: (s) => notifier.gorunumDegistir(s.first), ), IconButton( tooltip: 'Kapsamlı Prompt Sihirbazı', icon: const Icon(Icons.auto_fix_high, color: FikoRenkler.neonMor), onPressed: () => sihirbazDialogAc( context, baslik: 'Video Prompt Sihirbazı', ikon: Icons.auto_fix_high, renk: FikoRenkler.neonMor, icerik: Consumer(builder: (ctx, ref2, _) { final f = ref2.watch(videoFormProvider); return VideoSihirbazi( secimler: f.sihirbaz, onDegisti: ref2.read(videoFormProvider.notifier).sihirbazGuncelle, ); }), ), ), IconButton( tooltip: 'Üretim geçmişi', icon: Icon(Icons.history, color: ref.watch(videoGecmisAcikProvider) ? FikoRenkler.neonMor : null), onPressed: () => ref.read(videoGecmisAcikProvider.notifier).update((v) => !v), ), const SizedBox(width: 8), GestureDetector( onTap: () => notifier.nsfwAc(!form.nsfwAcik), child: Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6), decoration: BoxDecoration( color: form.nsfwAcik ? const Color(0xFFFF2E6A).withValues(alpha: 0.22) : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(7), border: Border.all( color: form.nsfwAcik ? const Color(0xFFFF2E6A) : Theme.of(context).dividerColor, width: form.nsfwAcik ? 1.3 : 1), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(form.nsfwAcik ? Icons.lock_open : Icons.lock, size: 13, color: form.nsfwAcik ? const Color(0xFFFF2E6A) : Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.5)), const SizedBox(width: 4), Text('18+', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w800, color: form.nsfwAcik ? const Color(0xFFFF2E6A) : Theme.of(context).colorScheme.onSurface)), ]), ), ), const Spacer(), const _UretButonu(), ], ); } } /// Video NSFW chip grupları — seçince cümleyi prompt'a toggle eder. class _NsfwVideoBolum extends ConsumerWidget { const _NsfwVideoBolum(); @override Widget build(BuildContext context, WidgetRef ref) { final prompt = ref.watch(videoFormProvider.select((s) => s.parametreler.prompt)); final notifier = ref.read(videoFormProvider.notifier); void toggle(KatalogOge o) { final p = ref.read(videoFormProvider).parametreler; final m = p.prompt; String yeni; if (m.contains(o.promptEn)) { yeni = m .replaceAll(', ${o.promptEn}', '') .replaceAll(o.promptEn, '') .replaceAll(RegExp(r',\s*,'), ',') .trim(); if (yeni.startsWith(',')) yeni = yeni.substring(1).trim(); } else { yeni = m.trim().isEmpty ? o.promptEn : '${m.trim()}, ${o.promptEn}'; } notifier.parametreGuncelle(p.kopyala(prompt: yeni)); } return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row(children: [ Icon(Icons.local_fire_department, size: 14, color: Color(0xFFFF2E6A)), SizedBox(width: 5), Text('🔞 NSFW', style: TextStyle( fontSize: 11.5, fontWeight: FontWeight.w700, color: Color(0xFFFF2E6A))), ]), for (final g in NsfwKataloglari.gruplar) ...[ Padding( padding: const EdgeInsets.only(top: 5, bottom: 2), child: Text(g.ad, style: TextStyle( fontSize: 10.5, fontWeight: FontWeight.w600, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.55))), ), Wrap( spacing: 5, runSpacing: 5, children: [ for (final o in g.ogeler) _MiniCip( etiket: o.etiketTr, secili: prompt.contains(o.promptEn), renk: const Color(0xFFFF2E6A), onTap: () => toggle(o), ), ], ), ], ], ), ); } } class _UretButonu extends ConsumerWidget { const _UretButonu(); @override Widget build(BuildContext context, WidgetRef ref) { return DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: const LinearGradient( colors: [FikoRenkler.neonMor, FikoRenkler.neonCyan]), boxShadow: [ BoxShadow( color: FikoRenkler.neonMor.withValues(alpha: 0.35), blurRadius: 14, offset: const Offset(0, 3)), ], ), child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: Colors.transparent, shadowColor: Colors.transparent, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), ), icon: const Icon(Icons.play_arrow_rounded, size: 18), label: const Text('Üret', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 13)), onPressed: () async { await ref.read(videoFormProvider.notifier).uret(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Video üretimi kuyruğa eklendi'), duration: Duration(seconds: 2), )); } }, ), ); } } class _HizliStilSatiri extends ConsumerWidget { const _HizliStilSatiri(); @override Widget build(BuildContext context, WidgetRef ref) { final notifier = ref.read(videoFormProvider.notifier); return SizedBox( height: 30, child: Row( children: [ const Icon(Icons.bolt, size: 14, color: FikoRenkler.neonAmber), const SizedBox(width: 4), const Text('Hazır:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), const SizedBox(width: 6), Expanded( child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: VideoKataloglari.hizliStiller.length, separatorBuilder: (_, _) => const SizedBox(width: 5), itemBuilder: (_, i) { final st = VideoKataloglari.hizliStiller[i]; return GestureDetector( onTap: () { final p = ref.read(videoFormProvider).parametreler; notifier.parametreGuncelle(p.kopyala(prompt: st.promptEn)); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(7), border: Border.all(color: Theme.of(context).dividerColor), ), child: Text(st.etiketTr, style: const TextStyle( fontSize: 11.5, fontWeight: FontWeight.w600)), ), ); }, ), ), ], ), ); } } class _PromptAlani extends ConsumerWidget { const _PromptAlani(); @override Widget build(BuildContext context, WidgetRef ref) { final prompt = ref.watch(videoFormProvider.select((s) => s.parametreler.prompt)); final notifier = ref.read(videoFormProvider.notifier); return TextFormField( key: ValueKey('video_prompt_${prompt.hashCode}'), initialValue: prompt, maxLines: 3, style: const TextStyle(fontSize: 12.5), decoration: const InputDecoration( labelText: 'Video Promptu (İngilizce)', hintText: 'a serene beach at sunset, gentle waves, cinematic, smooth motion...', alignLabelWithHint: true, ), onChanged: (v) { final p = ref.read(videoFormProvider).parametreler; notifier.parametreGuncelle(p.kopyala(prompt: v)); }, ); } } /// Tıklayınca cümlesini prompt'a ekleyen/çıkaran çip bölümü. class _CipBolum extends ConsumerWidget { const _CipBolum(this.baslik, this.ikon, this.renk, this.ogeler); final String baslik; final IconData ikon; final Color renk; final List ogeler; @override Widget build(BuildContext context, WidgetRef ref) { final prompt = ref.watch(videoFormProvider.select((s) => s.parametreler.prompt)); final notifier = ref.read(videoFormProvider.notifier); void toggle(VideoOge o) { final p = ref.read(videoFormProvider).parametreler; final mevcut = p.prompt; String yeni; if (mevcut.contains(o.promptEn)) { yeni = mevcut .replaceAll(', ${o.promptEn}', '') .replaceAll(o.promptEn, '') .replaceAll(RegExp(r',\s*,'), ',') .trim(); if (yeni.startsWith(',')) yeni = yeni.substring(1).trim(); } else { yeni = mevcut.trim().isEmpty ? o.promptEn : '${mevcut.trim()}, ${o.promptEn}'; } notifier.parametreGuncelle(p.kopyala(prompt: yeni)); } return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(ikon, size: 14, color: renk), const SizedBox(width: 5), Text(baslik, style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.w700)), ]), const SizedBox(height: 5), Wrap( spacing: 5, runSpacing: 5, children: [ for (final o in ogeler) _MiniCip( etiket: o.etiketTr, secili: prompt.contains(o.promptEn), renk: renk, onTap: () => toggle(o), ), ], ), ], ), ); } } class _Parametreler extends ConsumerWidget { const _Parametreler(); @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(videoFormProvider); final p = form.parametreler; final notifier = ref.read(videoFormProvider.notifier); final modeller = ref.watch(ltxModelleriProvider).valueOrNull ?? [p.model]; void ayarla(Map d) => notifier.parametreGuncelle( VideoParametreleri.fromJson({...p.toJson(), ...d})); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ DropdownButtonFormField( initialValue: modeller.contains(p.model) ? p.model : modeller.first, decoration: const InputDecoration(labelText: 'Model'), items: [ for (final m in modeller) DropdownMenuItem( value: m, child: Text(m.replaceAll('.safetensors', ''), overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11.5))), ], onChanged: (v) { if (v != null) ayarla({'model': v}); }, ), const SizedBox(height: 8), const Text('Oran / Çözünürlük', style: TextStyle(fontSize: 11.5, fontWeight: FontWeight.w600)), const SizedBox(height: 4), Wrap( spacing: 5, runSpacing: 5, children: [ for (final c in VideoKataloglari.cozunurlukler) _MiniCip( etiket: '${c.etiketTr} ${c.genislik}×${c.yukseklik}', secili: p.genislik == c.genislik && p.yukseklik == c.yukseklik, renk: FikoRenkler.neonCyan, onTap: () => ayarla({'genislik': c.genislik, 'yukseklik': c.yukseklik}), ), ], ), const SizedBox(height: 8), const Text('Süre', style: TextStyle(fontSize: 11.5, fontWeight: FontWeight.w600)), const SizedBox(height: 4), Wrap( spacing: 5, runSpacing: 5, children: [ for (final (etiket, frame) in VideoKataloglari.surePresetleri) _MiniCip( etiket: etiket, secili: p.uzunluk == frame, renk: FikoRenkler.neonPembe, onTap: () => ayarla({'uzunluk': frame}), ), ], ), const SizedBox(height: 8), // Sınırsız özel çözünürlük + süre (kullanıcı istediğini girer) Row( children: [ Expanded( child: _SayiAlani( etiket: 'Genişlik', deger: p.genislik, onChanged: (v) => ayarla({'genislik': (v ~/ 32 * 32).clamp(64, 100000)}), ), ), const SizedBox(width: 6), Expanded( child: _SayiAlani( etiket: 'Yükseklik', deger: p.yukseklik, onChanged: (v) => ayarla({'yukseklik': (v ~/ 32 * 32).clamp(64, 100000)}), ), ), const SizedBox(width: 6), Expanded( child: _SayiAlani( etiket: 'Süre (sn)', deger: (p.uzunluk / p.fps).round(), onChanged: (sn) { final ham = (sn * p.fps).round(); final uz = (((ham - 1) / 8).round().clamp(1, 100000)) * 8 + 1; ayarla({'uzunluk': uz}); }, ), ), ], ), Text( 'Sınırsız — istediğin değeri gir (otomatik geçerli ölçüye yuvarlanır)', style: TextStyle( fontSize: 9, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.45))), const SizedBox(height: 8), _SliderSatiri( etiket: 'Steps', deger: p.steps.toDouble(), min: 8, max: 40, bolum: 32, tam: true, onChanged: (v) => ayarla({'steps': v.round()}), ), _SliderSatiri( etiket: 'CFG', deger: p.cfg, min: 1, max: 8, bolum: 70, onChanged: (v) => ayarla({'cfg': v}), ), _SliderSatiri( etiket: 'FPS', deger: p.fps.toDouble(), min: 8, max: 30, bolum: 22, tam: true, onChanged: (v) => ayarla({'fps': v.round()}), ), Row( children: [ Expanded( child: SwitchListTile( dense: true, contentPadding: EdgeInsets.zero, title: const Text('Rastgele seed', style: TextStyle(fontSize: 12.5)), value: form.rastgeleSeed, onChanged: notifier.rastgeleSeedDegistir, ), ), Text('${(p.sureSn).toStringAsFixed(1)} sn', style: const TextStyle( fontSize: 11.5, color: FikoRenkler.neonAmber)), ], ), ], ); } } class _GelismisParametreler extends ConsumerWidget { const _GelismisParametreler(); @override Widget build(BuildContext context, WidgetRef ref) { final p = ref.watch(videoFormProvider.select((s) => s.parametreler)); final notifier = ref.read(videoFormProvider.notifier); final samplerlar = birlesikListe(ref.watch(comfySamplerlarProvider).valueOrNull, VideoKataloglari.samplerlar, p.samplerName); final schedulerlar = birlesikListe( ref.watch(comfySchedulerlarProvider).valueOrNull, VideoKataloglari.schedulerlar, p.scheduler); void ayarla(Map d) => notifier.parametreGuncelle( VideoParametreleri.fromJson({...p.toJson(), ...d})); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Divider(), const Text('Uzman', style: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w700, color: FikoRenkler.neonAmber)), _SliderSatiri( etiket: 'Max shift', deger: p.maxShift, min: 0.5, max: 4, bolum: 70, onChanged: (v) => ayarla({'maxShift': v})), _SliderSatiri( etiket: 'Base shift', deger: p.baseShift, min: 0.1, max: 2, bolum: 38, onChanged: (v) => ayarla({'baseShift': v})), Row( children: [ Expanded( child: DropdownButtonFormField( isExpanded: true, initialValue: p.samplerName, decoration: const InputDecoration(labelText: 'Sampler'), items: [ for (final s in samplerlar) DropdownMenuItem( value: s, child: Text(s, style: const TextStyle(fontSize: 11.5))), ], onChanged: (v) { if (v != null) ayarla({'samplerName': v}); }, ), ), const SizedBox(width: 10), Expanded( child: DropdownButtonFormField( isExpanded: true, initialValue: p.scheduler, decoration: const InputDecoration(labelText: 'Scheduler'), items: [ for (final s in schedulerlar) DropdownMenuItem( value: s, child: Text(s, style: const TextStyle(fontSize: 11.5))), ], onChanged: (v) { if (v != null) ayarla({'scheduler': v}); }, ), ), ], ), TextFormField( key: ValueKey('vid_neg_${p.negatif.hashCode}'), initialValue: p.negatif, decoration: InputDecoration( labelText: 'Negatif', suffixIcon: IconButton( tooltip: 'Negatif Sihirbazı', icon: const Icon(Icons.auto_fix_high, size: 18, color: FikoRenkler.neonPembe), onPressed: () => sihirbazDialogAc( context, baslik: 'Negatif Prompt Sihirbazı', ikon: Icons.block_rounded, renk: FikoRenkler.neonPembe, icerik: Consumer(builder: (ctx, ref2, _) { final p2 = ref2 .watch(videoFormProvider.select((s) => s.parametreler)); final nsfw = ref2.watch( videoFormProvider.select((s) => s.sihirbaz.nsfwAcik)); return NegatifSihirbazi( deger: p2.negatif, nsfw: nsfw, onDegisti: (v) => ref2 .read(videoFormProvider.notifier) .parametreGuncelle(VideoParametreleri.fromJson( {...p2.toJson(), 'negatif': v})), ); }), ), ), ), maxLines: 2, style: const TextStyle(fontSize: 11.5), onChanged: (v) => ayarla({'negatif': v}), ), ], ); } } /// I2V kaynak resim paneli — galeri/dosyadan resim yükle, video başlat. /// Kapsamlı video yan paneli: Kare Rehberleri + Ses + LoRA. class _VideoYanPanel extends ConsumerWidget { const _VideoYanPanel(); @override Widget build(BuildContext context, WidgetRef ref) { final p = ref.watch(videoFormProvider.select((s) => s.parametreler)); final n = ref.read(videoFormProvider.notifier); final rehberVar = p.kareRehberVar; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ───── Kare Rehberleri ───── Row(children: [ const Icon(Icons.filter_frames, size: 16, color: FikoRenkler.neonCyan), const SizedBox(width: 6), const Text('Kare Rehberleri', style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700)), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: (rehberVar ? FikoRenkler.neonCyan : FikoRenkler.neonMor) .withValues(alpha: 0.15), borderRadius: BorderRadius.circular(5), ), child: Text(rehberVar ? 'I2V' : 'T2V', style: TextStyle( fontSize: 9.5, fontWeight: FontWeight.w700, color: rehberVar ? FikoRenkler.neonCyan : FikoRenkler.neonMor)), ), ]), const SizedBox(height: 2), Text('Hepsi opsiyonel — hiçbiri yoksa metinden video (T2V)', style: TextStyle( fontSize: 9.5, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.5))), const SizedBox(height: 8), Row( children: [ _KareSlot(tip: 'ilk', etiket: 'İlk Kare', gorsel: p.etkinIlkKare), const SizedBox(width: 6), _KareSlot(tip: 'orta', etiket: 'Orta Kare', gorsel: p.ortaKare), const SizedBox(width: 6), _KareSlot(tip: 'son', etiket: 'Son Kare', gorsel: p.sonKare), ], ), if (rehberVar) ...[ const SizedBox(height: 6), Row(children: [ const Text('Rehber gücü', style: TextStyle(fontSize: 10.5)), Expanded( child: Slider( value: p.guideGuc.clamp(0.1, 1.0), min: 0.1, max: 1.0, onChanged: n.guideGucGuncelle, ), ), Text(p.guideGuc.toStringAsFixed(2), style: const TextStyle(fontSize: 10.5)), ]), ], const Divider(height: 18), // ───── Ses ───── Row(children: [ const Icon(Icons.graphic_eq, size: 16, color: FikoRenkler.neonAmber), const SizedBox(width: 6), const Text('Ses', style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700)), ]), const SizedBox(height: 6), SegmentedButton( style: const ButtonStyle( visualDensity: VisualDensity.compact, textStyle: WidgetStatePropertyAll(TextStyle(fontSize: 10.5))), segments: const [ ButtonSegment(value: SesModu.yok, label: Text('Yok')), ButtonSegment(value: SesModu.dosya, label: Text('Dosya')), ButtonSegment(value: SesModu.model, label: Text('Model')), ], selected: {p.sesModu}, onSelectionChanged: (s) => n.sesModuAyarla(s.first), ), if (p.sesModu == SesModu.dosya) ...[ const SizedBox(height: 6), Row(children: [ Expanded( child: Text( p.sesYerelYol.isEmpty ? 'Ses dosyası seçilmedi' : File(p.sesYerelYol).uri.pathSegments.last, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 10)), ), TextButton.icon( style: TextButton.styleFrom(visualDensity: VisualDensity.compact), icon: const Icon(Icons.audio_file, size: 15), label: const Text('Seç', style: TextStyle(fontSize: 11)), onPressed: () async { final secim = await FilePicker.platform .pickFiles(type: FileType.audio, dialogTitle: 'Ses seç'); final yol = secim?.files.firstOrNull?.path; if (yol != null && File(yol).existsSync()) { await n.sesDosyaSec(yol); } }, ), ]), if (p.sesYerelYol.isNotEmpty && p.sesToplamSure > 0) ...[ const SizedBox(height: 6), _SesSegmentSecici(p: p), ], ], if (p.sesModu == SesModu.model) Padding( padding: const EdgeInsets.only(top: 4), child: Text( 'Model sesi kendi üretir (LTX 2.x gerekir; 2b desteklemez)', style: TextStyle( fontSize: 9.5, color: FikoRenkler.neonAmber.withValues(alpha: 0.9))), ), const Divider(height: 18), // ───── LoRA ───── const _VideoLoraPanel(), ], ); } } /// Sınırsız sayı giriş alanı (commit: Enter veya odak kaybı). class _SayiAlani extends StatefulWidget { const _SayiAlani( {required this.etiket, required this.deger, required this.onChanged}); final String etiket; final int deger; final void Function(int) onChanged; @override State<_SayiAlani> createState() => _SayiAlaniState(); } class _SayiAlaniState extends State<_SayiAlani> { late final TextEditingController _ctrl = TextEditingController(text: '${widget.deger}'); @override void didUpdateWidget(_SayiAlani old) { super.didUpdateWidget(old); if (widget.deger != old.deger && _ctrl.text != '${widget.deger}') { _ctrl.text = '${widget.deger}'; } } @override void dispose() { _ctrl.dispose(); super.dispose(); } void _commit() { final v = int.tryParse(_ctrl.text.trim()); if (v != null && v > 0) widget.onChanged(v); } @override Widget build(BuildContext context) { return TextField( controller: _ctrl, keyboardType: TextInputType.number, style: const TextStyle(fontSize: 12), decoration: InputDecoration( labelText: widget.etiket, isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), ), onSubmitted: (_) => _commit(), onTapOutside: (_) { _commit(); FocusScope.of(context).unfocus(); }, ); } } /// Profesyonel ses segment seçici — dalga formu/spektrum + sürüklemeli başlangıç/bitiş. class _SesSegmentSecici extends ConsumerStatefulWidget { const _SesSegmentSecici({required this.p}); final VideoParametreleri p; @override ConsumerState<_SesSegmentSecici> createState() => _SesSegmentSeciciState(); } class _SesSegmentSeciciState extends ConsumerState<_SesSegmentSecici> { bool _spektrum = false; String? _gorselYol; String _yuklenenYol = ''; @override void initState() { super.initState(); _gorselGetir(); } @override void didUpdateWidget(_SesSegmentSecici old) { super.didUpdateWidget(old); if (old.p.sesYerelYol != widget.p.sesYerelYol || _spektrum != _sonSpektrum) { _gorselGetir(); } } bool _sonSpektrum = false; Future _gorselGetir() async { _sonSpektrum = _spektrum; final yol = widget.p.sesYerelYol; if (yol.isEmpty) return; final g = _spektrum ? await FfmpegServisi.spektrum(yol) : await FfmpegServisi.dalgaFormu(yol); if (mounted) { setState(() { _gorselYol = g; _yuklenenYol = yol; }); } } @override Widget build(BuildContext context) { final p = widget.p; final n = ref.read(videoFormProvider.notifier); final toplam = p.sesToplamSure; final bas = p.sesBaslangic.clamp(0, toplam).toDouble(); final bit = (p.sesBitis > bas ? p.sesBitis : (bas + p.sureSn)) .clamp(bas + 0.1, toplam) .toDouble(); String mmss(double s) { final dk = (s ~/ 60).toString().padLeft(2, '0'); final sn = (s % 60).toStringAsFixed(1).padLeft(4, '0'); return '$dk:$sn'; } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row(children: [ Text('Segment: ${mmss(bas)} → ${mmss(bit)} (${(bit - bas).toStringAsFixed(1)}sn)', style: const TextStyle(fontSize: 9.5, color: FikoRenkler.neonAmber)), const Spacer(), GestureDetector( onTap: () => setState(() { _spektrum = !_spektrum; _gorselGetir(); }), child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: _spektrum ? FikoRenkler.neonMor.withValues(alpha: 0.2) : Colors.transparent, borderRadius: BorderRadius.circular(5), border: Border.all(color: FikoRenkler.neonMor.withValues(alpha: 0.4)), ), child: Text(_spektrum ? 'Spektrum' : 'Dalga', style: const TextStyle(fontSize: 9, color: FikoRenkler.neonMor)), ), ), ]), const SizedBox(height: 4), LayoutBuilder(builder: (context, c) { final w = c.maxWidth; double xOf(double t) => toplam <= 0 ? 0 : (t / toplam) * w; void surukle(bool basMi, double dxDelta) { final dt = toplam <= 0 ? 0 : (dxDelta / w) * toplam; if (basMi) { n.sesSegmentAyarla((bas + dt).clamp(0, bit - 0.1).toDouble(), bit); } else { n.sesSegmentAyarla( bas, (bit + dt).clamp(bas + 0.1, toplam).toDouble()); } } return SizedBox( height: 56, child: Stack( children: [ // Dalga/spektrum görseli Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(6), child: Container( color: Colors.black26, child: (_gorselYol != null && _yuklenenYol == p.sesYerelYol) ? Image.file(File(_gorselYol!), fit: BoxFit.fill, gaplessPlayback: true, errorBuilder: (_, _, _) => const SizedBox()) : const Center( child: SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))), ), ), ), // Seçim dışı karartma (sol) Positioned( left: 0, top: 0, bottom: 0, width: xOf(bas), child: Container(color: Colors.black.withValues(alpha: 0.55)), ), // Seçim dışı karartma (sağ) Positioned( left: xOf(bit), top: 0, bottom: 0, right: 0, child: Container(color: Colors.black.withValues(alpha: 0.55)), ), // Seçim çerçevesi Positioned( left: xOf(bas), top: 0, bottom: 0, width: (xOf(bit) - xOf(bas)).clamp(1, w), child: Container( decoration: BoxDecoration( border: Border.all(color: FikoRenkler.neonAmber, width: 1.5), borderRadius: BorderRadius.circular(4), ), ), ), // Başlangıç kolu _Tutamac( x: xOf(bas), onDrag: (dx) => surukle(true, dx), ), // Bitiş kolu _Tutamac( x: xOf(bit), onDrag: (dx) => surukle(false, dx), ), ], ), ); }), ], ); } } /// Sürüklenebilir tutamaç (başlangıç/bitiş) — delta tabanlı. class _Tutamac extends StatelessWidget { const _Tutamac({required this.x, required this.onDrag}); final double x; final void Function(double dxDelta) onDrag; @override Widget build(BuildContext context) { return Positioned( left: x - 9, top: 0, bottom: 0, width: 18, child: GestureDetector( behavior: HitTestBehavior.translucent, onHorizontalDragUpdate: (d) => onDrag(d.delta.dx), child: MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, child: Center( child: Container( width: 4, decoration: BoxDecoration( color: FikoRenkler.neonAmber, borderRadius: BorderRadius.circular(2), boxShadow: [ BoxShadow( color: FikoRenkler.neonAmber.withValues(alpha: 0.6), blurRadius: 4) ], ), ), ), ), ), ); } } /// Tek kare rehberi slotu (yükle/önizle/kaldır). class _KareSlot extends ConsumerWidget { const _KareSlot( {required this.tip, required this.etiket, required this.gorsel}); final String tip; final String etiket; final String gorsel; @override Widget build(BuildContext context, WidgetRef ref) { final n = ref.read(videoFormProvider.notifier); final dolu = gorsel.trim().isNotEmpty; final url = ref.read(_comfyUrl); return Expanded( child: Column( children: [ Text(etiket, style: const TextStyle(fontSize: 9.5)), const SizedBox(height: 3), GestureDetector( onTap: () async { final secim = await FilePicker.platform .pickFiles(type: FileType.image, dialogTitle: '$etiket seç'); final yol = secim?.files.firstOrNull?.path; if (yol != null && File(yol).existsSync()) { await n.kareYukle(tip, yol); } }, child: AspectRatio( aspectRatio: 1, child: Container( 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 ? Stack(fit: StackFit.expand, children: [ Image.network( '$url/view?filename=${Uri.encodeComponent(gorsel)}&type=input', fit: BoxFit.cover, errorBuilder: (_, _, _) => const Icon(Icons.image, size: 24), ), Positioned( top: 1, right: 1, child: GestureDetector( onTap: () => n.kareKaldir(tip), child: Container( padding: const EdgeInsets.all(1), color: Colors.black54, child: const Icon(Icons.close, size: 12, color: Colors.white), ), ), ), ]) : Icon(Icons.add, size: 22, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.4)), ), ), ), ], ), ); } } /// Video LoRA paneli — kütüphaneden seçim + güç + kaldır. class _VideoLoraPanel extends ConsumerWidget { const _VideoLoraPanel(); @override Widget build(BuildContext context, WidgetRef ref) { final loralar = ref.watch(videoFormProvider.select((s) => s.parametreler.loralar)); final n = ref.read(videoFormProvider.notifier); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row(children: [ const Icon(Icons.layers, size: 16, color: FikoRenkler.neonYesil), const SizedBox(width: 6), const Text('LoRA', style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700)), const Spacer(), TextButton.icon( style: TextButton.styleFrom(visualDensity: VisualDensity.compact), icon: const Icon(Icons.add, size: 15), label: const Text('Ekle', style: TextStyle(fontSize: 11)), onPressed: () => _loraSec(context, ref), ), ]), if (loralar.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Text('LoRA yok', style: TextStyle( fontSize: 10, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.45))), ), for (var i = 0; i < loralar.length; i++) Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ Expanded( child: Text( loralar[i].dosya.replaceAll('.safetensors', ''), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 10.5)), ), SizedBox( width: 90, child: Slider( value: loralar[i].guc.clamp(0, 2), max: 2, onChanged: (v) => n.loraGucGuncelle(i, v), ), ), Text(loralar[i].guc.toStringAsFixed(2), style: const TextStyle(fontSize: 9.5)), IconButton( visualDensity: VisualDensity.compact, icon: const Icon(Icons.close, size: 14), onPressed: () => n.loraCikar(i), ), ], ), ), ], ); } void _loraSec(BuildContext context, WidgetRef ref) { final hepsi = ref.read(loraKayitlariProvider).valueOrNull ?? []; // Video için uyumlu: LTX/Wan/Hunyuan veya belirsiz, indirilmiş final uygun = hepsi .where((l) => l.indirildi && (l.tabanModel == TabanModel.ltx || l.tabanModel == TabanModel.wan || l.tabanModel == TabanModel.hunyuan || l.tabanModel == TabanModel.bilinmiyor)) .toList(); showDialog( context: context, builder: (ctx) => SimpleDialog( title: const Text('LoRA Seç (video uyumlu)'), children: [ if (uygun.isEmpty) const Padding( padding: EdgeInsets.all(16), child: Text( 'Uygun LoRA yok — LoRA Kütüphanesi\'nden indir/senkronla'), ), for (final l in uygun) SimpleDialogOption( onPressed: () { ref.read(videoFormProvider.notifier).loraEkle(VideoLora( dosya: l.dosya, guc: l.varsayilanGuc, trigger: l.tetik)); Navigator.pop(ctx); }, child: Row(children: [ if (l.nsfw) const Padding( padding: EdgeInsets.only(right: 6), child: Text('18+', style: TextStyle( fontSize: 9, fontWeight: FontWeight.w800, color: Color(0xFFFF2E6A))), ), Expanded(child: Text(l.ad, style: const TextStyle(fontSize: 12))), Text(LoraServisi.etiketler[l.tabanModel] ?? '', style: const TextStyle( fontSize: 10, color: FikoRenkler.neonCyan)), ]), ), ], ), ); } } // ComfyUI base URL'ini önizleme için al. final _comfyUrl = Provider((ref) { return ref.watch(ayarlarProvider).comfyUrl; }); class _SliderSatiri extends StatelessWidget { const _SliderSatiri({ required this.etiket, required this.deger, required this.min, required this.max, required this.bolum, required this.onChanged, this.tam = false, }); final String etiket; final double deger; final double min; final double max; final int bolum; final ValueChanged onChanged; final bool tam; @override Widget build(BuildContext context) { return Row( children: [ SizedBox( width: 80, child: Text(etiket, style: const TextStyle(fontSize: 12))), Expanded( child: Slider( value: deger.clamp(min, max), min: min, max: max, divisions: bolum, onChanged: onChanged, ), ), SizedBox( width: 42, child: Text( tam ? deger.round().toString() : deger.toStringAsFixed(2), textAlign: TextAlign.right, style: const TextStyle(fontSize: 11.5))), ], ); } } class _MiniCip extends StatelessWidget { const _MiniCip( {required this.etiket, required this.secili, required this.onTap, required this.renk}); final String etiket; final bool secili; final VoidCallback onTap; final Color renk; @override Widget build(BuildContext context) { final tema = Theme.of(context); return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 120), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3.5), decoration: BoxDecoration( color: secili ? renk.withValues(alpha: 0.22) : tema.colorScheme.surface, borderRadius: BorderRadius.circular(6), border: Border.all( color: secili ? renk : tema.dividerColor, width: secili ? 1.2 : 1), ), child: Text(etiket, style: TextStyle( fontSize: 11, height: 1.1, fontWeight: secili ? FontWeight.w700 : FontWeight.w500, color: secili ? renk : tema.colorScheme.onSurface)), ), ); } }