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 '../comfy/kuyruk_paneli.dart'; import '../model/model_servisi.dart' show comfySamplerlarProvider, comfySchedulerlarProvider, birlesikListe; import '../comfy/workflow/karakter_workflow.dart'; import '../comfy/workflow/resim_workflow.dart' show LoraSecimi; import '../galeri/studyo_gecmis.dart'; import '../lora/lora_servisi.dart'; import '../veri/isar_servisi.dart'; import '../veri/modeller/lora_kaydi.dart'; import '../veri/modeller/uretim_kaydi.dart'; import 'karakter_form_state.dart'; import 'karakter_kutuphanesi.dart'; final karakterGecmisAcikProvider = StateProvider((ref) => false); const _ipTipleri = [ 'linear', 'ease in', 'ease out', 'ease in-out', 'reverse in-out', 'weak input', 'weak output', 'strong middle', ]; const _samplerlar = [ 'euler', 'euler_ancestral', 'heun', 'dpmpp_2m', 'dpmpp_sde', 'ddim', ]; const _schedulerlar = [ 'simple', 'normal', 'karras', 'exponential', 'sgm_uniform', 'beta', 'ddim_uniform', 'kl_optimal', ]; const _karakterOranlar = [ ('1:1', 1024, 1024), ('3:4 Portre', 896, 1152), ('4:3', 1152, 896), ('9:16', 768, 1344), ('16:9', 1344, 768), ]; class KarakterEkrani extends ConsumerWidget { const KarakterEkrani({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const _UstBar(), const SizedBox(height: 8), const KarakterKutuphanesi(), const SizedBox(height: 8), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Sol: referanslar const SizedBox(width: 300, child: _ReferansPaneli()), const SizedBox(width: 12), // Orta: prompt + parametreler Expanded( child: Card( child: Padding( padding: const EdgeInsets.all(10), child: const SingleChildScrollView(child: _OrtaPanel()), ), ), ), if (ref.watch(karakterGecmisAcikProvider)) ...[ 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 f = ref.watch(karakterFormProvider); final n = ref.read(karakterFormProvider.notifier); return Row(children: [ const Icon(Icons.face_retouching_natural, color: FikoRenkler.neonYesil, size: 20), const SizedBox(width: 8), const Text('Karakter 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: {f.gelismis}, onSelectionChanged: (s) => n.gorunum(s.first), ), IconButton( tooltip: 'Üretim geçmişi', icon: Icon(Icons.history, color: ref.watch(karakterGecmisAcikProvider) ? FikoRenkler.neonMor : null), onPressed: () => ref.read(karakterGecmisAcikProvider.notifier).update((v) => !v), ), const Spacer(), DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: const LinearGradient( colors: [FikoRenkler.neonYesil, FikoRenkler.neonCyan]), ), child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: Colors.transparent, shadowColor: Colors.transparent), icon: const Icon(Icons.auto_awesome, size: 18), label: const Text('Üret', style: TextStyle(fontWeight: FontWeight.w800)), onPressed: n.uret, ), ), ]); } } class _ReferansPaneli extends StatelessWidget { const _ReferansPaneli(); @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: const [ Text('Referans Görseller', style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w800)), SizedBox(height: 8), _RefSlot(tip: 'karakter', baslik: 'Karakter (Yüz)', aciklama: 'PuLID — yüz tutarlılığı', ikon: Icons.face, renk: FikoRenkler.neonYesil), _RefSlot(tip: 'kiyafet', baslik: 'Kıyafet', aciklama: 'IPAdapter — giysi referansı', ikon: Icons.checkroom, renk: FikoRenkler.neonPembe), _RefSlot(tip: 'mekan', baslik: 'Mekan', aciklama: 'IPAdapter — ortam referansı', ikon: Icons.landscape, renk: FikoRenkler.neonCyan), _RefSlot(tip: 'stil', baslik: 'Stil', aciklama: 'IPAdapter — genel stil', ikon: Icons.palette, renk: FikoRenkler.neonMor), ], ), ), ); } } class _RefSlot extends ConsumerWidget { const _RefSlot( {required this.tip, required this.baslik, required this.aciklama, required this.ikon, required this.renk}); final String tip, baslik, aciklama; final IconData ikon; final Color renk; @override Widget build(BuildContext context, WidgetRef ref) { final r = ref.watch(karakterFormProvider.select((s) => switch (tip) { 'karakter' => s.parametreler.karakter, 'kiyafet' => s.parametreler.kiyafet, 'mekan' => s.parametreler.mekan, _ => s.parametreler.stil, })); final n = ref.read(karakterFormProvider.notifier); final url = ref.read(_comfyUrlK); final dolu = r.aktif; return Padding( padding: const EdgeInsets.only(bottom: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, 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 Spacer(), if (dolu) GestureDetector( onTap: () => n.refKaldir(tip), child: const Icon(Icons.close, size: 14), ), ]), Text(aciklama, style: TextStyle(fontSize: 9, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45))), const SizedBox(height: 4), GestureDetector( onTap: () async { final s = await FilePicker.platform.pickFiles( type: FileType.image, dialogTitle: '$baslik görseli seç'); final yol = s?.files.firstOrNull?.path; if (yol != null && File(yol).existsSync()) await n.refYukle(tip, yol); }, child: Container( height: 70, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all(color: dolu ? renk.withValues(alpha: 0.6) : Theme.of(context).dividerColor), ), clipBehavior: Clip.antiAlias, child: dolu ? Image.network('$url/view?filename=${Uri.encodeComponent(r.gorsel)}&type=input', fit: BoxFit.cover, errorBuilder: (_, _, _) => const Icon(Icons.image, size: 28)) : Icon(Icons.add_photo_alternate_outlined, size: 26, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.35)), ), ), if (dolu) Row(children: [ const Text('Güç', style: TextStyle(fontSize: 9.5)), Expanded( child: Slider( value: r.guc.clamp(0, 1.5), max: 1.5, onChanged: (v) => n.refGuc(tip, v), ), ), Text(r.guc.toStringAsFixed(2), style: const TextStyle(fontSize: 9.5)), ]), ], ), ); } } class _OrtaPanel extends ConsumerWidget { const _OrtaPanel(); @override Widget build(BuildContext context, WidgetRef ref) { final f = ref.watch(karakterFormProvider); final p = f.parametreler; final n = ref.read(karakterFormProvider.notifier); final modeller = ref.watch(karakterModelleriProvider).valueOrNull ?? [p.model]; final samplerlar = birlesikListe( ref.watch(comfySamplerlarProvider).valueOrNull, _samplerlar, p.samplerName); final schedulerlar = birlesikListe( ref.watch(comfySchedulerlarProvider).valueOrNull, _schedulerlar, p.scheduler); KarakterParametreleri pp(Map d) => KarakterParametreleri.fromJson({...p.toJson(), ...d}); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( key: ValueKey('kprompt_${p.prompt.hashCode}'), initialValue: p.prompt, minLines: 2, maxLines: 4, decoration: const InputDecoration( labelText: 'Sahne / aksiyon promptu (İngilizce)', hintText: 'the same character as a knight in a forest, cinematic...', isDense: true), style: const TextStyle(fontSize: 12.5), onChanged: (v) => n.guncelle(p.kopyala(prompt: v)), ), const SizedBox(height: 8), DropdownButtonFormField( initialValue: modeller.contains(p.model) ? p.model : modeller.first, decoration: const InputDecoration(labelText: 'Model (Flux)', isDense: true), 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) n.guncelle(pp({'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 (et, gn, yk) in _karakterOranlar) _Cip(et, p.genislik == gn && p.yukseklik == yk, () => n.guncelle(pp({'genislik': gn, 'yukseklik': yk}))), ]), const SizedBox(height: 6), Row(children: [ Expanded(child: _Sayi('Genişlik', p.genislik, (v) => n.guncelle(pp({'genislik': (v ~/ 32 * 32).clamp(64, 100000)})))), const SizedBox(width: 6), Expanded(child: _Sayi('Yükseklik', p.yukseklik, (v) => n.guncelle(pp({'yukseklik': (v ~/ 32 * 32).clamp(64, 100000)})))), ]), const SizedBox(height: 8), _Slider('Steps', p.steps.toDouble(), 8, 50, true, (v) => n.guncelle(pp({'steps': v.round()}))), _Slider('Guidance', p.guidance, 1, 8, false, (v) => n.guncelle(pp({'guidance': v}))), if (f.gelismis) ...[ _Slider('CFG', p.cfg, 1, 6, false, (v) => n.guncelle(pp({'cfg': v}))), _Slider('Batch', p.batch.toDouble(), 1, 8, true, (v) => n.guncelle(pp({'batch': v.round()}))), DropdownButtonFormField( initialValue: p.ipWeightType, decoration: const InputDecoration(labelText: 'IPAdapter ağırlık tipi', isDense: true), items: [for (final t in _ipTipleri) DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 11.5)))], onChanged: (v) { if (v != null) n.guncelle(pp({'ipWeightType': v})); }, ), const SizedBox(height: 6), Row(children: [ Expanded(child: DropdownButtonFormField( isExpanded: true, initialValue: p.samplerName, decoration: const InputDecoration(labelText: 'Sampler', isDense: true), items: [for (final s in samplerlar) DropdownMenuItem(value: s, child: Text(s, style: const TextStyle(fontSize: 11.5)))], onChanged: (v) { if (v != null) n.guncelle(pp({'samplerName': v})); }, )), const SizedBox(width: 6), Expanded(child: DropdownButtonFormField( isExpanded: true, initialValue: p.scheduler, decoration: const InputDecoration(labelText: 'Scheduler', isDense: true), items: [for (final s in schedulerlar) DropdownMenuItem(value: s, child: Text(s, style: const TextStyle(fontSize: 11.5)))], onChanged: (v) { if (v != null) n.guncelle(pp({'scheduler': v})); }, )), ]), const SizedBox(height: 6), TextFormField( key: ValueKey('kar_neg_${p.negatif.hashCode}'), initialValue: p.negatif, maxLines: 2, style: const TextStyle(fontSize: 11.5), decoration: InputDecoration( labelText: 'Negatif Prompt', isDense: true, 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 neg = ref2.watch(karakterFormProvider .select((s) => s.parametreler.negatif)); return NegatifSihirbazi( deger: neg, onDegisti: (v) { final cur = ref2.read(karakterFormProvider).parametreler; ref2 .read(karakterFormProvider.notifier) .guncelle(cur.kopyala(negatif: v)); }, ); }), ), ), ), onChanged: (v) => n.guncelle(pp({'negatif': v})), ), ], const Divider(height: 18), const _KarakterLora(), ], ); } } class _KarakterLora extends ConsumerWidget { const _KarakterLora(); @override Widget build(BuildContext context, WidgetRef ref) { final loralar = ref.watch(karakterFormProvider.select((s) => s.parametreler.loralar)); final n = ref.read(karakterFormProvider.notifier); return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row(children: [ const Icon(Icons.layers, size: 15, color: FikoRenkler.neonYesil), const SizedBox(width: 5), const Text('LoRA', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700)), const Spacer(), TextButton.icon( style: TextButton.styleFrom(visualDensity: VisualDensity.compact), icon: const Icon(Icons.add, size: 14), label: const Text('Ekle', style: TextStyle(fontSize: 11)), onPressed: () => _sec(context, ref), ), ]), for (var i = 0; i < loralar.length; i++) 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.loraGuc(i, v))), IconButton(visualDensity: VisualDensity.compact, icon: const Icon(Icons.close, size: 14), onPressed: () => n.loraCikar(i)), ]), ]); } void _sec(BuildContext context, WidgetRef ref) { final hepsi = ref.read(loraKayitlariProvider).valueOrNull ?? []; final uygun = hepsi.where((l) => l.indirildi && (l.tabanModel == TabanModel.flux || l.tabanModel == TabanModel.bilinmiyor)).toList(); showDialog(context: context, builder: (ctx) => SimpleDialog( title: const Text('LoRA Seç (Flux uyumlu)'), children: [ if (uygun.isEmpty) const Padding(padding: EdgeInsets.all(16), child: Text('Uygun LoRA yok — LoRA Kütüphanesi\'nden indir')), for (final l in uygun) SimpleDialogOption( onPressed: () { ref.read(karakterFormProvider.notifier) .loraEkle(LoraSecimi(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))), ]), ), ], )); } } final _comfyUrlK = Provider((ref) => ref.watch(ayarlarProvider).comfyUrl); class _Cip extends StatelessWidget { const _Cip(this.etiket, this.secili, this.onTap); final String etiket; final bool secili; final VoidCallback onTap; @override Widget build(BuildContext context) { final t = Theme.of(context); return GestureDetector(onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4), decoration: BoxDecoration( color: secili ? FikoRenkler.neonYesil.withValues(alpha: 0.2) : t.colorScheme.surface, borderRadius: BorderRadius.circular(7), border: Border.all(color: secili ? FikoRenkler.neonYesil : t.dividerColor), ), child: Text(etiket, style: TextStyle(fontSize: 11, fontWeight: secili ? FontWeight.w700 : FontWeight.w500, color: secili ? FikoRenkler.neonYesil : t.colorScheme.onSurface)), )); } } class _Sayi extends StatefulWidget { const _Sayi(this.etiket, this.deger, this.onChanged); final String etiket; final int deger; final void Function(int) onChanged; @override State<_Sayi> createState() => _SayiState(); } class _SayiState extends State<_Sayi> { late final _c = TextEditingController(text: '${widget.deger}'); @override void didUpdateWidget(_Sayi 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 _Slider extends StatelessWidget { const _Slider(this.etiket, this.deger, this.min, this.max, this.tam, this.onChanged); final String etiket; final double deger, min, max; final bool tam; final void Function(double) onChanged; @override Widget build(BuildContext context) => Row(children: [ SizedBox(width: 64, child: Text(etiket, style: const TextStyle(fontSize: 11))), Expanded(child: Slider(value: deger.clamp(min, max), min: min, max: max, onChanged: onChanged)), SizedBox(width: 32, child: Text(tam ? '${deger.round()}' : deger.toStringAsFixed(1), style: const TextStyle(fontSize: 10.5), textAlign: TextAlign.end)), ]); }