import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../cekirdek/kimlik.dart'; import '../../cekirdek/video_kataloglari.dart'; import '../../modeller/segment.dart'; import '../../modeller/video_sihirbaz_sablon.dart'; import '../../modeller/wangp_ayarlari.dart'; import '../../servisler/lora/lora_indirici.dart'; import '../../servisler/wangp/video_sihirbaz_sablonlari.dart'; import '../../servisler/wangp/wangp_profilleri.dart'; import '../ortak/lora_yoneticisi_modal.dart'; /// Video segmenti için ayrıntılı sihirbaz — **hem LTX Desktop hem WanGP**. /// /// Müzik sihirbazına benzer mantık: kullanıcı chip ve slider'larla /// sahne/karakter/stil/ışık/atmosfer seçer; çıktı = segment ayarları /// (prompt + negatif + cözünürlük + cfg + WanGP'de LoRA stack + profil). /// /// LTX motorunda profil/LoRA bölümleri gizli, WanGP'de tam görünür. /// Şablonlar her iki motor için ortak. class WanGpVideoSihirbazi extends ConsumerStatefulWidget { final Segment segment; final void Function(Segment Function(Segment)) guncelle; const WanGpVideoSihirbazi({ super.key, required this.segment, required this.guncelle, }); static Future goster( BuildContext ctx, { required Segment segment, required void Function(Segment Function(Segment)) guncelle, }) { return showDialog( context: ctx, builder: (c) => Dialog( insetPadding: const EdgeInsets.all(20), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1100, maxHeight: 820), child: WanGpVideoSihirbazi( segment: segment, guncelle: guncelle, ), ), ), ); } @override ConsumerState createState() => _WanGpVideoSihirbaziState(); } class _WanGpVideoSihirbaziState extends ConsumerState { // ── Seçim state'i ── String _hedefProfilId = 'sys_4090_serin'; String _cozunurlukKod = '720p_h'; final Set _sahneTipleri = {}; final Set _kameraHareketleri = {}; final Set _karakterTipleri = {}; final Set _ifadeler = {}; final Set _aksiyonlar = {}; final Set _giyimler = {}; final Set _ortamlar = {}; final Set _isiklandirmalar = {}; final Set _gorselStiller = {}; final Set _atmosferler = {}; int _yogunluk = 60; int _detaySeviyesi = 50; double _yaraticilik = 1.0; final List _loraDosyalari = []; final List _loraMultipliers = []; late final TextEditingController _serbestNotCtrl; late final TextEditingController _negatifEkCtrl; int? _seedSabit; // Şablon listesi List _sablonlar = const []; String? _aktifSablonId; // LoRA listesi (sistemde mevcut) List _mevcutLoraDosyalari = const []; /// Hive box adı — sihirbaz son durumu otomatik kaydedilir. static const _sonDurumSablonId = 'sys_son_durum_otomatik'; @override void initState() { super.initState(); _serbestNotCtrl = TextEditingController(); _negatifEkCtrl = TextEditingController(); _onYukle(); } /// Açılışta son sihirbaz durumunu (chip'ler, slider'lar, LoRA seçimi) /// otomatik geri yükle. Bu kullanıcının iş akışını korur — her açılışta /// boş chip listesi yerine son seçimleri hazır gelir. Future _sonDurumYukle() async { final son = await VideoSihirbazSablonServisi.bul(_sonDurumSablonId); if (son != null && mounted) { _sablonYukle(son); // _aktifSablonId reset — "son durum" sistem şablonu, vurgulu olmasın setState(() => _aktifSablonId = null); } } /// Çıkışta veya uygulama anında son durumu otomatik kaydet. Future _sonDurumKaydet() async { final sablon = VideoSihirbazSablon( id: _sonDurumSablonId, ad: '⏱ Son Durum (otomatik)', aciklama: 'Sihirbazda yaptığın son seçim — açılışta otomatik yüklenir', hedefProfilId: _hedefProfilId, cozunurlukKod: _cozunurlukKod, sahneTipleri: _sahneTipleri, kameraHareketleri: _kameraHareketleri, karakterTipleri: _karakterTipleri, ifadeler: _ifadeler, aksiyonlar: _aksiyonlar, giyimler: _giyimler, ortamlar: _ortamlar, isiklandirmalar: _isiklandirmalar, gorselStiller: _gorselStiller, atmosferler: _atmosferler, yogunluk: _yogunluk, detaySeviyesi: _detaySeviyesi, yaraticilik: _yaraticilik, loraDosyalari: List.of(_loraDosyalari), loraMultipliers: List.of(_loraMultipliers), serbestNot: _serbestNotCtrl.text, negatifEk: _negatifEkCtrl.text, seedSabit: _seedSabit, ); await VideoSihirbazSablonServisi.kaydet(sablon); } @override void dispose() { // Dialog kapatılırken son durumu otomatik kaydet (fire-and-forget) _sonDurumKaydet(); _serbestNotCtrl.dispose(); _negatifEkCtrl.dispose(); super.dispose(); } Future _onYukle() async { final sablonlar = await VideoSihirbazSablonServisi.liste(); final mevcutLoralar = LoraIndirici.mevcutLoralar([ LoraIndirici.wangpLoraDizini, LoraIndirici.wangpLoraI2vDizini, LoraIndirici.wangpLoraWanDizini, LoraIndirici.wangpLoraLtx2Dizini, ]).map((f) { return f.path.split(RegExp(r'[\\/]')).last; }).toList(); if (mounted) { setState(() { // "Son Durum" sistem şablonunu kullanıcı listesinden gizle (iç state) _sablonlar = sablonlar .where((s) => s.id != _sonDurumSablonId) .toList(); _mevcutLoraDosyalari = mevcutLoralar; }); } // Son durumu otomatik geri yükle await _sonDurumYukle(); } // ── Sonuç hesaplama ──────────────────────────────────────────────────── ({String prompt, String negatif, WanGpAyarlari ayarlar}) _hesapla() { final prc = []; // Stil ilk gelir prc.addAll(VideoKatalog.gorselStiller .where((s) => _gorselStiller.contains(s.kod)) .map((s) => s.promptKarsiligi)); // Sahne / kamera prc.addAll(VideoKatalog.sahneTipleri .where((s) => _sahneTipleri.contains(s.kod)) .map((s) => s.promptKarsiligi)); prc.addAll(VideoKatalog.kameraHareketleri .where((s) => _kameraHareketleri.contains(s.kod)) .map((s) => s.promptKarsiligi)); // Karakter prc.addAll(VideoKatalog.karakterTipleri .where((s) => _karakterTipleri.contains(s.kod)) .map((s) => s.promptKarsiligi)); prc.addAll(VideoKatalog.ifadeler .where((s) => _ifadeler.contains(s.kod)) .map((s) => s.promptKarsiligi)); prc.addAll(VideoKatalog.aksiyonlar .where((s) => _aksiyonlar.contains(s.kod)) .map((s) => s.promptKarsiligi)); prc.addAll(VideoKatalog.giyimler .where((s) => _giyimler.contains(s.kod)) .map((s) => s.promptKarsiligi)); // Ortam / ışık / atmosfer prc.addAll(VideoKatalog.ortamlar .where((s) => _ortamlar.contains(s.kod)) .map((s) => s.promptKarsiligi)); prc.addAll(VideoKatalog.isiklandirmalar .where((s) => _isiklandirmalar.contains(s.kod)) .map((s) => s.promptKarsiligi)); prc.addAll(VideoKatalog.atmosferler .where((s) => _atmosferler.contains(s.kod)) .map((s) => s.promptKarsiligi)); // Yoğunluk → atmosfer şiddet kelimeleri if (_yogunluk >= 80) { prc.add('extremely intense atmosphere, dramatic energy'); } else if (_yogunluk >= 60) { prc.add('strong emotional impact'); } else if (_yogunluk <= 30) { prc.add('gentle subtle atmosphere'); } // Detay seviyesi → kalite kelimeleri if (_detaySeviyesi >= 75) { prc.add( 'ultra detailed, intricate textures, sharp focus, 8K resolution, masterpiece'); } else if (_detaySeviyesi >= 50) { prc.add('high detail, sharp focus, professional quality'); } else if (_detaySeviyesi <= 25) { prc.add('clean simple composition'); } // Serbest not en sona final not = _serbestNotCtrl.text.trim(); if (not.isNotEmpty) prc.add(not); final prompt = prc.join(', '); // ── NEGATİF PROMPT (akıllı, durum-bazlı) ────────────────────────── // 1) Genel kalite kelimeleri (her zaman ekle) final negSet = { 'blurry', 'low quality', 'distorted face', 'extra fingers', 'mutated hands', 'multiple people', 'bad anatomy', 'oversaturated', 'jpeg artifacts', 'watermark', 'text overlay', }; // 2) Karakter cinsiyet/yaş kontrolü — istemeyenleri engelle if (_karakterTipleri.contains('child') || _karakterTipleri.contains('teen')) { negSet.addAll(['adult', 'old person', 'wrinkles']); } if (_karakterTipleri.contains('adult') || _karakterTipleri.contains('young_adult')) { negSet.addAll(['child', 'baby', 'infant']); } if (_karakterTipleri.contains('senior')) { negSet.addAll(['child', 'youthful skin']); } // 3) Tüm seçimlere göre çelişki tablosundan ekle // Kullanıcı bir özellik seçtiyse, mantıksal zıttını negatife koy. final tumSecimler = { ..._gorselStiller, ..._atmosferler, ..._ifadeler, ..._aksiyonlar, ..._isiklandirmalar, ..._sahneTipleri, ..._ortamlar, ..._giyimler, ..._kameraHareketleri, }; for (final secim in tumSecimler) { final celiskiler = VideoKatalog.negatifCelismeler[secim]; if (celiskiler != null) negSet.addAll(celiskiler); } // 4) Kullanıcının manuel eklemesi final negatifEk = _negatifEkCtrl.text.trim(); if (negatifEk.isNotEmpty) negSet.add(negatifEk); // 5) Çakışma temizleme — pozitif prompt'ta zaten var olan negatif'i çıkar // (Örn: "anime" stilinde "photorealistic" eklenir; ama kullanıcı serbest // notta "photorealistic" yazdıysa çıkar.) final pozLower = prompt.toLowerCase(); negSet.removeWhere( (n) => pozLower.contains(n.toLowerCase()) && n.length > 3); final negatif = negSet.join(', '); // Profil temel ayarlarını al final profil = WanGpHazirProfiller.tumu .firstWhere((p) => p.id == _hedefProfilId, orElse: () => WanGpHazirProfiller.rtx4090Serin); final cozunurluk = VideoKatalog.cozunurlukler .firstWhere((c) => c.kod == _cozunurlukKod, orElse: () => VideoKatalog.cozunurlukler[2]); // Yaratıcılık → CFG ayar (bazı profillerde override var) final cfg = (1.0 + _yaraticilik * 4.0).clamp(1.0, 7.0); // Profil ayarlarını kopyala + sihirbaz override'ları final ayarlar = profil.ayarlar.copyWith( guidanceScale: cfg, loraDosyalari: List.of(_loraDosyalari), loraMultipliers: List.of(_loraMultipliers), ozelAlanlar: { ...profil.ayarlar.ozelAlanlar, if (profil.vramSafetyCoefficient != null) 'vram_safety_coefficient': profil.vramSafetyCoefficient, 'resolution': cozunurluk.boyut, }, ); return (prompt: prompt, negatif: negatif, ayarlar: ayarlar); } void _segmenteUygula() { final h = _hesapla(); final cozunurluk = VideoKatalog.cozunurlukler .firstWhere((c) => c.kod == _cozunurlukKod, orElse: () => VideoKatalog.cozunurlukler[2]); final isLtx = widget.segment.motor == SegmentMotor.ltx; // Çözünürlük string mapping: 832x480 → "480p", vb. String? cozBucket; if (cozunurluk.boyut.startsWith('832x') || cozunurluk.boyut.startsWith('480x')) { cozBucket = '512p'; } else if (cozunurluk.boyut.startsWith('960x')) { cozBucket = '540p'; } else if (cozunurluk.boyut.startsWith('1280x') || cozunurluk.boyut.startsWith('704x')) { cozBucket = '720p'; } else if (cozunurluk.boyut.startsWith('1920x') || cozunurluk.boyut.startsWith('1088x')) { cozBucket = '1080p'; } widget.guncelle((s) => s.copyWith( prompt: h.prompt, negatifPrompt: h.negatif, // WanGP'ye özel detaylı ayarlar — LTX motorunda atlanır wangpAyarlari: isLtx ? s.wangpAyarlari : h.ayarlar, // Ortak alanlar her iki motor için oran: cozunurluk.oran, cozunurluk: cozBucket ?? s.cozunurluk, guidanceScale: h.ayarlar.guidanceScale, steps: (h.ayarlar.ozelAlanlar['num_inference_steps'] as int?) ?? s.steps, seed: _seedSabit ?? s.seed, )); // Uygulama anında son durumu da kaydet (sihirbaz tekrar açılınca hatırlasın) // ignore: unawaited_futures _sonDurumKaydet(); if (mounted) { final motorAd = isLtx ? 'LTX Desktop' : 'WanGP'; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Sihirbaz ayarları $motorAd segmentine uygulandı ✓'), duration: const Duration(seconds: 2), ), ); Navigator.of(context).pop(); } } Future _sablonOlarakKaydet() async { final adCtrl = TextEditingController(text: _onerilenAd()); final aciklamaCtrl = TextEditingController(); final onay = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Şablon olarak kaydet'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: adCtrl, autofocus: true, decoration: const InputDecoration(labelText: 'Şablon adı'), ), const SizedBox(height: 8), TextField( controller: aciklamaCtrl, maxLines: 2, decoration: const InputDecoration( labelText: 'Açıklama (opsiyonel)'), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('İptal')), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Kaydet')), ], ), ); if (onay == true && adCtrl.text.trim().isNotEmpty) { final id = _aktifSablonId ?? 'usr_${FikoYTId.yeni()}'; final sablon = VideoSihirbazSablon( id: id, ad: adCtrl.text.trim(), aciklama: aciklamaCtrl.text.trim(), hedefProfilId: _hedefProfilId, cozunurlukKod: _cozunurlukKod, sahneTipleri: _sahneTipleri, kameraHareketleri: _kameraHareketleri, karakterTipleri: _karakterTipleri, ifadeler: _ifadeler, aksiyonlar: _aksiyonlar, giyimler: _giyimler, ortamlar: _ortamlar, isiklandirmalar: _isiklandirmalar, gorselStiller: _gorselStiller, atmosferler: _atmosferler, yogunluk: _yogunluk, detaySeviyesi: _detaySeviyesi, yaraticilik: _yaraticilik, loraDosyalari: List.of(_loraDosyalari), loraMultipliers: List.of(_loraMultipliers), serbestNot: _serbestNotCtrl.text, negatifEk: _negatifEkCtrl.text, seedSabit: _seedSabit, ); await VideoSihirbazSablonServisi.kaydet(sablon); _aktifSablonId = id; await _onYukle(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('"${sablon.ad}" kaydedildi')), ); } } } void _sablonYukle(VideoSihirbazSablon s) { setState(() { _aktifSablonId = s.id; _hedefProfilId = s.hedefProfilId; _cozunurlukKod = s.cozunurlukKod; _sahneTipleri ..clear() ..addAll(s.sahneTipleri); _kameraHareketleri ..clear() ..addAll(s.kameraHareketleri); _karakterTipleri ..clear() ..addAll(s.karakterTipleri); _ifadeler ..clear() ..addAll(s.ifadeler); _aksiyonlar ..clear() ..addAll(s.aksiyonlar); _giyimler ..clear() ..addAll(s.giyimler); _ortamlar ..clear() ..addAll(s.ortamlar); _isiklandirmalar ..clear() ..addAll(s.isiklandirmalar); _gorselStiller ..clear() ..addAll(s.gorselStiller); _atmosferler ..clear() ..addAll(s.atmosferler); _yogunluk = s.yogunluk; _detaySeviyesi = s.detaySeviyesi; _yaraticilik = s.yaraticilik; _loraDosyalari ..clear() ..addAll(s.loraDosyalari); _loraMultipliers ..clear() ..addAll(s.loraMultipliers); _serbestNotCtrl.text = s.serbestNot; _negatifEkCtrl.text = s.negatifEk; _seedSabit = s.seedSabit; }); } Future _sablonSil(VideoSihirbazSablon s) async { final onay = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Şablonu sil?'), content: Text('"${s.ad}" silinecek. Devam?'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('İptal')), FilledButton( style: FilledButton.styleFrom(backgroundColor: Colors.red), onPressed: () => Navigator.pop(ctx, true), child: const Text('Sil')), ], ), ); if (onay == true) { await VideoSihirbazSablonServisi.sil(s.id); if (_aktifSablonId == s.id) _aktifSablonId = null; await _onYukle(); } } String _onerilenAd() { final p = []; if (_gorselStiller.isNotEmpty) { final ilk = VideoKatalog.gorselStiller .firstWhere((x) => _gorselStiller.contains(x.kod)); p.add(ilk.etiket); } if (_atmosferler.isNotEmpty) { final ilk = VideoKatalog.atmosferler .firstWhere((x) => _atmosferler.contains(x.kod)); p.add(ilk.etiket); } if (_aksiyonlar.isNotEmpty) { final ilk = VideoKatalog.aksiyonlar .firstWhere((x) => _aksiyonlar.contains(x.kod)); p.add(ilk.etiket); } return p.isEmpty ? 'Yeni Şablon' : p.join(' · '); } // ── BUILD ───────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { final theme = Theme.of(context); final h = _hesapla(); final isLtx = widget.segment.motor == SegmentMotor.ltx; final motorAd = isLtx ? 'LTX Desktop' : 'WanGP'; final motorRenk = isLtx ? Colors.purple : Colors.green; return Column( mainAxisSize: MainAxisSize.min, children: [ // Header Padding( padding: const EdgeInsets.fromLTRB(16, 12, 12, 8), child: Row( children: [ Icon(Icons.auto_fix_high, color: theme.colorScheme.primary, size: 24), const SizedBox(width: 8), Text('Video Üretim Sihirbazı', style: theme.textTheme.titleLarge), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: motorRenk.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), border: Border.all( color: motorRenk.withValues(alpha: 0.5), width: 1), ), child: Text(motorAd, style: TextStyle( fontSize: 11, color: motorRenk, fontWeight: FontWeight.w700)), ), const SizedBox(width: 12), if (_aktifSablonId != null) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3), decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: Text( 'Şablon yüklü', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.w600), ), ), const Spacer(), IconButton( icon: const Icon(Icons.bookmark_add_outlined), tooltip: 'Bu seçimleri şablon olarak kaydet', onPressed: _sablonOlarakKaydet, ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), ], ), ), const Divider(height: 1), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // SOL — seçim alanları Expanded( flex: 3, child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isLtx) ...[ _baslik(theme, 'Hedef Profil', 'Üretim kalitesi/hızı dengesi (WanGP)'), _profilSecici(theme), const SizedBox(height: 12), ], _baslik(theme, 'Çözünürlük', null), _cozunurlukSecici(theme), const SizedBox(height: 16), _baslik(theme, 'Görsel Stil', 'Stiller arası seçim — biri yeterli'), _chipGrubu(VideoKatalog.gorselStiller, _gorselStiller), const SizedBox(height: 16), _baslik(theme, 'Sahne Tipi / Kompozisyon', 'Plan ölçeği, kadraj'), _chipGrubu(VideoKatalog.sahneTipleri, _sahneTipleri), const SizedBox(height: 16), _baslik(theme, 'Kamera Hareketi', 'Birden fazla seçebilirsin (zincirleme)'), _chipGrubu( VideoKatalog.kameraHareketleri, _kameraHareketleri), const SizedBox(height: 16), _baslik(theme, 'Karakter Tipi (yaş)', null), _chipGrubu( VideoKatalog.karakterTipleri, _karakterTipleri), const SizedBox(height: 12), _baslik(theme, 'Karakter İfadesi', null), _chipGrubu(VideoKatalog.ifadeler, _ifadeler), const SizedBox(height: 12), _baslik(theme, 'Karakter Aksiyonu', 'Şarkı söylüyorsa "Şarkı Söylüyor"'), _chipGrubu(VideoKatalog.aksiyonlar, _aksiyonlar), const SizedBox(height: 12), _baslik(theme, 'Giyim (opsiyonel)', null), _chipGrubu(VideoKatalog.giyimler, _giyimler), const SizedBox(height: 16), _baslik(theme, 'Ortam / Lokasyon', null), _chipGrubu(VideoKatalog.ortamlar, _ortamlar), const SizedBox(height: 12), _baslik(theme, 'Işıklandırma', 'Atmosferin %50\'sini ışık belirler'), _chipGrubu( VideoKatalog.isiklandirmalar, _isiklandirmalar), const SizedBox(height: 12), _baslik(theme, 'Atmosfer / Ruh Hali', null), _chipGrubu(VideoKatalog.atmosferler, _atmosferler), const SizedBox(height: 16), _baslik(theme, 'Atmosfer Yoğunluğu', '0=hafif, 100=yoğun'), _slider(theme, _yogunluk.toDouble(), 0, 100, 5, (v) => setState(() => _yogunluk = v.round())), const SizedBox(height: 8), _baslik(theme, 'Detay Seviyesi', 'Yüksek = "ultra detailed" eklenir'), _slider(theme, _detaySeviyesi.toDouble(), 0, 100, 5, (v) => setState(() => _detaySeviyesi = v.round())), const SizedBox(height: 8), _baslik(theme, 'Yaratıcılık (CFG)', 'Düşük = prompt\'a bağlı, Yüksek = serbest yorum'), _slider(theme, _yaraticilik, 0.5, 1.5, 0.05, (v) => setState(() => _yaraticilik = v), formatter: (v) => v.toStringAsFixed(2)), const SizedBox(height: 16), if (!isLtx) ...[ _baslik(theme, 'LoRA Stack (mevcut dosyalardan)', 'Birden fazla seçilebilir, multiplier toplam ≤2.0 (WanGP)'), _loraSecici(theme), const SizedBox(height: 16), ], _baslik(theme, 'Serbest Not (prompt eki)', null), TextField( controller: _serbestNotCtrl, maxLines: 2, decoration: const InputDecoration( hintText: 'Örn: "neon reflections on wet ground, motion blur"', border: OutlineInputBorder(), isDense: true, ), onChanged: (_) => setState(() {}), ), const SizedBox(height: 12), _baslik(theme, 'Negatif Prompt Eki', null), TextField( controller: _negatifEkCtrl, decoration: const InputDecoration( hintText: 'Örn: "smoking, drinking, weapons"', border: OutlineInputBorder(), isDense: true, ), onChanged: (_) => setState(() {}), ), const SizedBox(height: 12), Row( children: [ Checkbox( value: _seedSabit != null, onChanged: (v) => setState(() { _seedSabit = v == true ? 42 : null; }), ), const Text('Sabit seed (tekrar üretilebilirlik)', style: TextStyle(fontSize: 12)), const SizedBox(width: 8), if (_seedSabit != null) SizedBox( width: 100, child: TextFormField( key: ValueKey('seed-$_seedSabit'), initialValue: _seedSabit?.toString() ?? '', keyboardType: TextInputType.number, decoration: const InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric( horizontal: 8, vertical: 8), ), onChanged: (v) => setState(() { _seedSabit = int.tryParse(v) ?? 42; }), ), ), ], ), ], ), ), ), const VerticalDivider(width: 1), // SAĞ — önizleme + şablonlar + uygula Expanded( flex: 2, child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _onizlemeKart(theme, 'Pozitif Prompt', h.prompt, theme.colorScheme.primary), const SizedBox(height: 8), _onizlemeKart(theme, 'Negatif', h.negatif, theme.colorScheme.error), const SizedBox(height: 8), _profilOzet(theme, h.ayarlar), const SizedBox(height: 8), Expanded(child: _sablonListesi(theme)), const SizedBox(height: 8), Row( children: [ Expanded( child: OutlinedButton.icon( icon: const Icon(Icons.bookmark_add, size: 16), label: const Text('Şablon Kaydet'), onPressed: _sablonOlarakKaydet, ), ), const SizedBox(width: 8), Expanded( flex: 2, child: FilledButton.icon( icon: const Icon(Icons.check, size: 16), label: const Text('Segmente Uygula'), onPressed: _segmenteUygula, ), ), ], ), ], ), ), ), ], ), ), ], ); } // ── Yardımcı widget'lar ──────────────────────────────────────────────── Widget _baslik(ThemeData theme, String baslik, String? alt) { return Padding( padding: const EdgeInsets.only(top: 4, bottom: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(baslik, style: theme.textTheme.titleSmall ?.copyWith(fontWeight: FontWeight.w700)), if (alt != null && alt.isNotEmpty) Text(alt, style: theme.textTheme.labelSmall ?.copyWith(color: theme.colorScheme.outline)), ], ), ); } Widget _profilSecici(ThemeData theme) { return Wrap( spacing: 6, runSpacing: 6, children: WanGpHazirProfiller.tumu.map((p) { final aktif = p.id == _hedefProfilId; return ChoiceChip( label: Text(p.ad, style: TextStyle( fontSize: 11, fontWeight: aktif ? FontWeight.w700 : FontWeight.normal)), selected: aktif, onSelected: (s) { if (s) setState(() => _hedefProfilId = p.id); }, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ); }).toList(), ); } Widget _cozunurlukSecici(ThemeData theme) { return Wrap( spacing: 6, children: VideoKatalog.cozunurlukler.map((c) { final aktif = c.kod == _cozunurlukKod; return ChoiceChip( label: Text(c.etiket, style: const TextStyle(fontSize: 11)), selected: aktif, onSelected: (s) { if (s) setState(() => _cozunurlukKod = c.kod); }, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ); }).toList(), ); } Widget _chipGrubu(List kataloglar, Set secili) { return Wrap( spacing: 4, runSpacing: 4, children: kataloglar.map((s) { final aktif = secili.contains(s.kod); return FilterChip( label: Text(s.etiket, style: const TextStyle(fontSize: 11)), selected: aktif, onSelected: (sec) { setState(() { if (sec) { secili.add(s.kod); } else { secili.remove(s.kod); } }); }, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ); }).toList(), ); } Widget _slider( ThemeData theme, double deger, double min, double max, double adim, ValueChanged onChanged, { String Function(double)? formatter, }) { final raw = deger.clamp(min, max); final adimSayisi = ((raw - min) / adim).round(); final d = (min + adimSayisi * adim).clamp(min, max); return Row( children: [ Expanded( child: Slider( value: d, min: min, max: max, divisions: ((max - min) / adim).round().clamp(1, 200), label: formatter?.call(d) ?? d.toStringAsFixed(0), onChanged: onChanged, ), ), SizedBox( width: 50, child: Text(formatter?.call(d) ?? d.toStringAsFixed(0), textAlign: TextAlign.right, style: theme.textTheme.labelSmall), ), ], ); } Widget _loraSecici(ThemeData theme) { if (_mevcutLoraDosyalari.isEmpty) { return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), ), child: Row( children: [ Expanded( child: Text( 'Mevcut LoRA yok. LoRA Yöneticisi\'nden indir.', style: theme.textTheme.bodySmall, ), ), TextButton.icon( icon: const Icon(Icons.download, size: 14), label: const Text('LoRA İndir', style: TextStyle(fontSize: 11)), onPressed: () => LoraYoneticisiModal.goster( context, LoraModulu.video), ), ], ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Wrap( spacing: 6, runSpacing: 6, children: _mevcutLoraDosyalari.map((dosya) { final aktif = _loraDosyalari.contains(dosya); return FilterChip( label: Text(dosya, style: const TextStyle(fontSize: 10)), selected: aktif, onSelected: (sec) { setState(() { if (sec && !aktif) { _loraDosyalari.add(dosya); _loraMultipliers.add(1.0); } else if (!sec && aktif) { final i = _loraDosyalari.indexOf(dosya); _loraDosyalari.removeAt(i); if (i < _loraMultipliers.length) { _loraMultipliers.removeAt(i); } } }); }, ); }).toList(), ), if (_loraDosyalari.isNotEmpty) ...[ const SizedBox(height: 8), ...List.generate(_loraDosyalari.length, (i) { final mul = i < _loraMultipliers.length ? _loraMultipliers[i] : 1.0; return Row( children: [ SizedBox( width: 130, child: Text(_loraDosyalari[i], overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 10)), ), Expanded( child: Slider( value: mul, min: 0, max: 2, divisions: 40, label: mul.toStringAsFixed(2), onChanged: (v) { setState(() { while (_loraMultipliers.length < _loraDosyalari.length) { _loraMultipliers.add(1.0); } _loraMultipliers[i] = v; }); }, ), ), SizedBox( width: 36, child: Text(mul.toStringAsFixed(2), style: const TextStyle(fontSize: 10), textAlign: TextAlign.right), ), ], ); }), ], ], ); } Widget _onizlemeKart( ThemeData theme, String baslik, String icerik, Color renk) { return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: renk.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(6), border: Border.all(color: renk.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(baslik, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: renk, letterSpacing: 0.5)), const SizedBox(height: 4), Text( icerik.isEmpty ? '(boş)' : icerik, style: const TextStyle(fontSize: 11, height: 1.3), maxLines: 4, overflow: TextOverflow.ellipsis, ), ], ), ); } Widget _profilOzet(ThemeData theme, WanGpAyarlari ayar) { final profil = WanGpHazirProfiller.tumu .firstWhere((p) => p.id == _hedefProfilId, orElse: () => WanGpHazirProfiller.rtx4090Serin); return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: theme.colorScheme.tertiary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(6), border: Border.all(color: theme.colorScheme.tertiary.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('UYGULANAN PROFİL', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: theme.colorScheme.tertiary, letterSpacing: 0.5)), const SizedBox(height: 2), Text(profil.ad, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600)), const SizedBox(height: 2), Wrap( spacing: 4, runSpacing: 2, children: [ _miniChip('CFG ${ayar.guidanceScale?.toStringAsFixed(1) ?? "—"}'), _miniChip( 'Steps ${ayar.ozelAlanlar['num_inference_steps'] ?? "—"}'), _miniChip( 'Res ${ayar.ozelAlanlar['resolution'] ?? "—"}'), if (ayar.loraDosyalari.isNotEmpty) _miniChip('${ayar.loraDosyalari.length} LoRA'), ], ), ], ), ); } Widget _miniChip(String text) { return Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(3), ), child: Text(text, style: const TextStyle(fontSize: 9)), ); } Widget _sablonListesi(ThemeData theme) { if (_sablonlar.isEmpty) { return Container( alignment: Alignment.center, padding: const EdgeInsets.all(20), child: Text('Henüz şablon yok\nKaydet butonuyla oluştur', textAlign: TextAlign.center, style: theme.textTheme.bodySmall ?.copyWith(color: theme.colorScheme.outline)), ); } return Container( decoration: BoxDecoration( border: Border.all( color: theme.colorScheme.outline.withValues(alpha: 0.3)), borderRadius: BorderRadius.circular(6), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.all(8), child: Text('KAYITLI ŞABLONLAR', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: theme.colorScheme.outline, letterSpacing: 0.5)), ), const Divider(height: 1), Expanded( child: ListView.builder( itemCount: _sablonlar.length, itemBuilder: (c, i) { final s = _sablonlar[i]; final aktif = _aktifSablonId == s.id; return ListTile( dense: true, selected: aktif, title: Text(s.ad, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600)), subtitle: s.aciklama.isNotEmpty ? Text(s.aciklama, style: const TextStyle(fontSize: 10), maxLines: 1, overflow: TextOverflow.ellipsis) : null, onTap: () => _sablonYukle(s), trailing: IconButton( icon: const Icon(Icons.delete_outline, size: 16), visualDensity: VisualDensity.compact, onPressed: () => _sablonSil(s), ), ); }, ), ), ], ), ); } }