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 '../model/model_servisi.dart' show comfySamplerlarProvider, comfySchedulerlarProvider, birlesikListe; import '../comfy/kuyruk_paneli.dart'; import '../comfy/workflow/resim_workflow.dart'; import '../galeri/studyo_gecmis.dart'; import '../veri/isar_servisi.dart'; import '../veri/modeller/uretim_kaydi.dart'; import 'lora_panel.dart'; import 'resim_form_state.dart'; import 'resim_kataloglari.dart'; import 'resim_sihirbazi.dart'; import 'resim_sihirbaz_secimleri.dart'; final resimGecmisAcikProvider = StateProvider((ref) => false); /// Yüz değiştirme (FaceSwap/Reactor) referans satırı. class _YuzDegistirSatiri extends ConsumerWidget { const _YuzDegistirSatiri(); @override Widget build(BuildContext context, WidgetRef ref) { final yuz = ref.watch(resimFormProvider.select((s) => s.parametreler.yuzKaynak)); final n = ref.read(resimFormProvider.notifier); final dolu = yuz.trim().isNotEmpty; final url = ref.watch(ayarlarProvider).comfyUrl; return Row(children: [ const Icon(Icons.face_retouching_natural, size: 16, color: FikoRenkler.neonPembe), const SizedBox(width: 6), const Text('Yüz Değiştir', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700)), const SizedBox(width: 6), Text('(üretilen resme referans yüz)', style: TextStyle(fontSize: 9.5, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45))), const Spacer(), if (dolu) ...[ Container(width: 34, height: 34, decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), border: Border.all(color: FikoRenkler.neonPembe.withValues(alpha: 0.6))), clipBehavior: Clip.antiAlias, child: Image.network('$url/view?filename=${Uri.encodeComponent(yuz)}&type=input', fit: BoxFit.cover, errorBuilder: (_, _, _) => const Icon(Icons.face, size: 18))), IconButton(visualDensity: VisualDensity.compact, icon: const Icon(Icons.close, size: 15), onPressed: n.yuzKaynakKaldir), ] else OutlinedButton.icon( style: OutlinedButton.styleFrom(visualDensity: VisualDensity.compact), icon: const Icon(Icons.upload_file, size: 15), label: const Text('Yüz yükle', style: TextStyle(fontSize: 11.5)), onPressed: () async { final s = await FilePicker.platform.pickFiles( type: FileType.image, dialogTitle: 'Değiştirilecek yüz görseli'); final yol = s?.files.firstOrNull?.path; if (yol != null && File(yol).existsSync()) await n.yuzKaynakYukle(yol); }, ), ]); } } class ResimEkrani extends ConsumerWidget { const ResimEkrani({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(resimFormProvider); 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( child: Card( child: Padding( padding: const EdgeInsets.all(10), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const _StilPaketSatiri(), const SizedBox(height: 8), const _ModSecici(), const SizedBox(height: 8), const _PromptAlani(), const SizedBox(height: 6), const _NegatifAlani(), const SizedBox(height: 10), const _ModelOranSatiri(), const SizedBox(height: 10), const LoraPanel(), const SizedBox(height: 8), const _YuzDegistirSatiri(), const Divider(height: 18), const _TemelParametreler(), if (form.gelismisGorunum) ...[ const SizedBox(height: 8), const _GelismisParametreler(), ], ], ), ), ), ), ), if (ref.watch(resimGecmisAcikProvider)) ...[ const SizedBox(width: 12), const StudyoGecmisPaneli(tip: UretimTipi.resim), ], ], ), ), const SizedBox(height: 10), const KuyrukPaneli(tipFiltresi: UretimTipi.resim), ], ), ); } } class _UstBar extends ConsumerWidget { const _UstBar(); @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(resimFormProvider); final notifier = ref.read(resimFormProvider.notifier); return Row( children: [ const Icon(Icons.image_rounded, color: FikoRenkler.neonPembe, size: 20), const SizedBox(width: 8), const Text('Resim 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), ), const Spacer(), IconButton( tooltip: 'Prompt Sihirbazı', icon: const Icon(Icons.auto_fix_high, color: FikoRenkler.neonMor), onPressed: () => sihirbazDialogAc( context, baslik: 'Resim Prompt Sihirbazı', ikon: Icons.auto_fix_high, renk: FikoRenkler.neonPembe, icerik: const ResimSihirbazi(), ), ), IconButton( tooltip: 'Üretim geçmişi', icon: Icon(Icons.history, color: ref.watch(resimGecmisAcikProvider) ? FikoRenkler.neonMor : null), onPressed: () => ref.read(resimGecmisAcikProvider.notifier).update((v) => !v), ), const SizedBox(width: 8), const _UretButonu(), ], ); } } 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.neonPembe, FikoRenkler.neonMor]), boxShadow: [ BoxShadow( color: FikoRenkler.neonPembe.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.auto_awesome, size: 18), label: const Text('Üret', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 13)), onPressed: () async { await ref.read(resimFormProvider.notifier).uret(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Resim üretimi kuyruğa eklendi'), duration: Duration(seconds: 2), )); } }, ), ); } } /// Hızlı stil paketleri (acemiye 1-tık hazır şablon). class _StilPaketSatiri extends ConsumerWidget { const _StilPaketSatiri(); @override Widget build(BuildContext context, WidgetRef ref) { final notifier = ref.read(resimFormProvider.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: ResimKataloglari.stilPaketleri.length, separatorBuilder: (_, _) => const SizedBox(width: 5), itemBuilder: (_, i) { final pkt = ResimKataloglari.stilPaketleri[i]; return Tooltip( message: pkt.aciklama, child: GestureDetector( onTap: () => notifier.stilPaketiUygula(pkt), 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(pkt.etiketTr, style: const TextStyle( fontSize: 11.5, fontWeight: FontWeight.w600)), ), ), ); }, ), ), ], ), ); } } /// Karakter / Mekan / Ürün / Sanat mod seçici. class _ModSecici extends ConsumerWidget { const _ModSecici(); @override Widget build(BuildContext context, WidgetRef ref) { final modu = ref.watch(resimFormProvider.select((s) => s.sihirbaz.modu)); final notifier = ref.read(resimFormProvider.notifier); return SegmentedButton( style: const ButtonStyle( visualDensity: VisualDensity.compact, textStyle: WidgetStatePropertyAll(TextStyle(fontSize: 12))), segments: const [ ButtonSegment( value: ResimModu.karakter, label: Text('👤 Karakter'), icon: Icon(Icons.person, size: 14)), ButtonSegment( value: ResimModu.mekan, label: Text('🏞️ Mekan'), icon: Icon(Icons.landscape, size: 14)), ButtonSegment( value: ResimModu.urun, label: Text('📦 Ürün'), icon: Icon(Icons.shopping_bag, size: 14)), ButtonSegment( value: ResimModu.sanat, label: Text('🎨 Sanat'), icon: Icon(Icons.palette, size: 14)), ], selected: {modu}, onSelectionChanged: (s) => notifier.modDegistir(s.first), ); } } class _PromptAlani extends ConsumerWidget { const _PromptAlani(); @override Widget build(BuildContext context, WidgetRef ref) { final prompt = ref.watch(resimFormProvider.select((s) => s.parametreler.prompt)); final notifier = ref.read(resimFormProvider.notifier); return TextFormField( key: ValueKey('resim_prompt_${prompt.hashCode}'), initialValue: prompt, maxLines: 3, style: const TextStyle(fontSize: 12.5), decoration: const InputDecoration( labelText: 'Resim Promptu (İngilizce)', hintText: 'Sihirbaz seçimleri buraya derlenir, elle de düzenleyebilirsin...', alignLabelWithHint: true, ), onChanged: (v) { final p = ref.read(resimFormProvider).parametreler; notifier.parametreGuncelle(p.kopyalaTam(prompt: v)); }, ); } } class _NegatifAlani extends ConsumerWidget { const _NegatifAlani(); @override Widget build(BuildContext context, WidgetRef ref) { final neg = ref.watch(resimFormProvider.select((s) => s.parametreler.negatif)); final notifier = ref.read(resimFormProvider.notifier); return TextFormField( key: ValueKey('resim_neg_${neg.hashCode}'), initialValue: neg, maxLines: 2, style: const TextStyle(fontSize: 11.5), decoration: InputDecoration( labelText: 'Negatif Prompt', hintText: 'blurry, low quality, deformed hands...', 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(resimFormProvider.select((s) => s.parametreler)); final nsfw = ref2.watch( resimFormProvider.select((s) => s.sihirbaz.nsfwAcik)); return NegatifSihirbazi( deger: p2.negatif, nsfw: nsfw, onDegisti: (v) => ref2 .read(resimFormProvider.notifier) .parametreGuncelle(p2.kopyalaTam(negatif: v)), ); }), ), ), ), onChanged: (v) { final p = ref.read(resimFormProvider).parametreler; notifier.parametreGuncelle(p.kopyalaTam(negatif: v)); }, ); } } class _ModelOranSatiri extends ConsumerWidget { const _ModelOranSatiri(); @override Widget build(BuildContext context, WidgetRef ref) { final p = ref.watch(resimFormProvider.select((s) => s.parametreler)); final notifier = ref.read(resimFormProvider.notifier); final modeller = ref.watch(fluxModelleriProvider).valueOrNull ?? [p.model]; void ayarla(Map degis) => notifier.parametreGuncelle( ResimParametreleri.fromJson({...p.toJson(), ...degis})); 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), SizedBox( height: 30, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: ResimKataloglari.cozunurlukler.length, separatorBuilder: (_, _) => const SizedBox(width: 5), itemBuilder: (_, i) { final c = ResimKataloglari.cozunurlukler[i]; final aktif = p.genislik == c.genislik && p.yukseklik == c.yukseklik; return GestureDetector( onTap: () => ayarla( {'genislik': c.genislik, 'yukseklik': c.yukseklik}), child: Container( padding: const EdgeInsets.symmetric( horizontal: 9, vertical: 5), decoration: BoxDecoration( color: aktif ? FikoRenkler.neonPembe.withValues(alpha: 0.22) : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(7), border: Border.all( color: aktif ? FikoRenkler.neonPembe : Theme.of(context).dividerColor), ), child: Text( '${c.etiketTr} ${c.genislik}×${c.yukseklik}', style: TextStyle( fontSize: 11.5, fontWeight: aktif ? FontWeight.w700 : FontWeight.w500, color: aktif ? FikoRenkler.neonPembe : Theme.of(context).colorScheme.onSurface)), ), ); }, ), ), const SizedBox(height: 6), Row(children: [ Expanded(child: _SayiKutuR('Genişlik', p.genislik, (v) => ayarla({'genislik': (v ~/ 32 * 32).clamp(64, 100000)}))), const SizedBox(width: 6), Expanded(child: _SayiKutuR('Yükseklik', p.yukseklik, (v) => ayarla({'yukseklik': (v ~/ 32 * 32).clamp(64, 100000)}))), const SizedBox(width: 6), Expanded(child: Text('Sınırsız — istediğin değeri gir', style: TextStyle(fontSize: 9, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45)))), ]), ], ); } } /// Sınırsız sayı girişi (resim). class _SayiKutuR extends StatefulWidget { const _SayiKutuR(this.etiket, this.deger, this.onChanged); final String etiket; final int deger; final void Function(int) onChanged; @override State<_SayiKutuR> createState() => _SayiKutuRState(); } class _SayiKutuRState extends State<_SayiKutuR> { late final _c = TextEditingController(text: '${widget.deger}'); @override void didUpdateWidget(_SayiKutuR o) { super.didUpdateWidget(o); if (widget.deger != o.deger && _c.text != '${widget.deger}') { _c.text = '${widget.deger}'; } } @override void dispose() { _c.dispose(); super.dispose(); } void _commit() { final v = int.tryParse(_c.text.trim()); if (v != null && v > 0) widget.onChanged(v); } @override Widget build(BuildContext context) => TextField( controller: _c, 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(); }, ); } class _TemelParametreler extends ConsumerWidget { const _TemelParametreler(); @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(resimFormProvider); final p = form.parametreler; final notifier = ref.read(resimFormProvider.notifier); void ayarla(String alan, num d) => notifier.parametreGuncelle( ResimParametreleri.fromJson({...p.toJson(), alan: d})); Widget slider(String etiket, String alan, double deger, double min, double max, int bolum, {bool tam = false}) { 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: (v) => ayarla(alan, tam ? v.round() : v), ), ), SizedBox( width: 42, child: Text( tam ? deger.round().toString() : deger.toStringAsFixed(2), textAlign: TextAlign.right, style: const TextStyle(fontSize: 11.5))), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ slider('Steps', 'steps', p.steps.toDouble(), 4, 50, 46, tam: true), slider('Guidance', 'guidance', p.guidance, 0, 10, 100), slider('Batch (varyant)', 'batch', p.batch.toDouble(), 1, 8, 7, tam: true), const SizedBox(height: 4), 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, ), ), Expanded( child: Row( children: [ const Text('Seed:', style: TextStyle(fontSize: 11.5)), const SizedBox(width: 6), Expanded( child: Text('${p.seed}', overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11.5)), ), IconButton( tooltip: 'Yeni seed', visualDensity: VisualDensity.compact, icon: const Icon(Icons.casino, size: 18), onPressed: () => notifier.parametreGuncelle( p.kopyala(seed: ResimParametreleri.rastgeleSeed())), ), ], ), ), ], ), ], ); } } class _GelismisParametreler extends ConsumerWidget { const _GelismisParametreler(); static const _samplerlar = [ 'euler', 'euler_ancestral', 'heun', 'dpmpp_2m', 'dpmpp_sde', 'ddim', ]; static const _schedulerlar = [ 'simple', 'normal', 'karras', 'exponential', 'sgm_uniform', 'beta', 'ddim_uniform', 'kl_optimal', ]; @override Widget build(BuildContext context, WidgetRef ref) { final p = ref.watch(resimFormProvider.select((s) => s.parametreler)); final notifier = ref.read(resimFormProvider.notifier); final samplerlar = birlesikListe( ref.watch(comfySamplerlarProvider).valueOrNull, _samplerlar, p.samplerName); final schedulerlar = birlesikListe( ref.watch(comfySchedulerlarProvider).valueOrNull, _schedulerlar, p.scheduler); void ayarla(Map degis) => notifier.parametreGuncelle( ResimParametreleri.fromJson({...p.toJson(), ...degis})); Widget slider(String etiket, String alan, double deger, double min, double max, int bolum, {bool tam = false}) { return Row( children: [ SizedBox( width: 95, child: Text(etiket, style: const TextStyle(fontSize: 12))), Expanded( child: Slider( value: deger.clamp(min, max), min: min, max: max, divisions: bolum, onChanged: (v) => ayarla({alan: tam ? v.round() : v}), ), ), SizedBox( width: 46, child: Text( tam ? deger.round().toString() : deger.toStringAsFixed(2), textAlign: TextAlign.right, style: const TextStyle(fontSize: 11.5))), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Divider(), const Padding( padding: EdgeInsets.symmetric(vertical: 4), child: Text('Uzman Parametreleri', style: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w700, color: FikoRenkler.neonAmber)), ), slider('CFG', 'cfg', p.cfg, 1, 10, 90), slider('Denoise', 'denoise', p.denoise, 0, 1, 100), Row( children: [ SizedBox( width: 95, child: Row(children: [ const SizedBox(width: 4), const Text('Genişlik', style: TextStyle(fontSize: 12)), ])), Expanded( child: Slider( value: p.genislik.toDouble().clamp(384, 2048), min: 384, max: 2048, divisions: 52, onChanged: (v) => ayarla({'genislik': (v / 32).round() * 32}), ), ), SizedBox( width: 46, child: Text('${p.genislik}', textAlign: TextAlign.right, style: const TextStyle(fontSize: 11.5))), ], ), Row( children: [ SizedBox( width: 95, child: const Text('Yükseklik', style: TextStyle(fontSize: 12))), Expanded( child: Slider( value: p.yukseklik.toDouble().clamp(384, 2048), min: 384, max: 2048, divisions: 52, onChanged: (v) => ayarla({'yukseklik': (v / 32).round() * 32}), ), ), SizedBox( width: 46, child: Text('${p.yukseklik}', textAlign: TextAlign.right, style: const TextStyle(fontSize: 11.5))), ], ), const SizedBox(height: 6), 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}); }, ), ), ], ), ], ); } }