import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/tema.dart'; import '../comfy/comfy_istemci.dart'; import '../comfy/workflow/lora_egitim_workflow.dart'; import '../veri/isar_servisi.dart'; import '../veri/modeller/karakter.dart'; import 'karakter_form_state.dart'; import 'karakter_servisi.dart'; const _cinsiyetler = ['kadın', 'erkek', 'genç kız', 'genç erkek', 'androjen']; const _yaslar = ['çocuk', 'genç (18-25)', 'yetişkin (25-40)', 'orta yaş', 'yaşlı']; const _vucutlar = ['ince', 'atletik', 'kaslı', 'dolgun', 'balık etli', 'zayıf']; const _stiller = [ 'photorealistic', 'cinematic', 'anime', 'oil painting', '3d render', 'comic', 'fantasy art', 'cyberpunk', ]; /// Üst bardaki isimli karakter kütüphanesi şeridi. class KarakterKutuphanesi extends ConsumerWidget { const KarakterKutuphanesi({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final karakterler = ref.watch(karakterlerProvider).valueOrNull ?? []; return Card( child: Padding( padding: const EdgeInsets.fromLTRB(10, 6, 10, 6), child: Row(children: [ const Icon(Icons.groups, size: 16, color: FikoRenkler.neonYesil), const SizedBox(width: 6), const Text('Karakter Kütüphanesi', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700)), const SizedBox(width: 10), OutlinedButton.icon( style: OutlinedButton.styleFrom(visualDensity: VisualDensity.compact), icon: const Icon(Icons.person_add, size: 15), label: const Text('Yeni', style: TextStyle(fontSize: 11.5)), onPressed: () => _duzenle(context, ref, Karakter()), ), const SizedBox(width: 10), Expanded( child: SizedBox( height: 58, child: karakterler.isEmpty ? Align( alignment: Alignment.centerLeft, child: Text('Henüz karakter yok — "Yeni" ile oluştur', style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurface .withValues(alpha: 0.45)))) : ListView.separated( scrollDirection: Axis.horizontal, itemCount: karakterler.length, separatorBuilder: (_, _) => const SizedBox(width: 8), itemBuilder: (_, i) => _KarakterKart(karakter: karakterler[i]), ), ), ), ]), ), ); } } class _KarakterKart extends ConsumerWidget { const _KarakterKart({required this.karakter}); final Karakter karakter; @override Widget build(BuildContext context, WidgetRef ref) { final url = ref.read(_comfyUrlKK); final egitim = ref.watch(loraEgitimDurumProvider)[karakter.id]; return GestureDetector( onTap: () => ref.read(karakterFormProvider.notifier).karakteriUygula(karakter), child: Container( width: 168, padding: const EdgeInsets.all(5), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all(color: karakter.loraAdi.isNotEmpty ? FikoRenkler.neonMor.withValues(alpha: 0.5) : Theme.of(context).dividerColor), ), child: Row(children: [ Container( width: 46, height: 46, decoration: BoxDecoration( color: Colors.black26, borderRadius: BorderRadius.circular(6)), clipBehavior: Clip.antiAlias, child: karakter.anaRef.isNotEmpty ? Image.network('$url/view?filename=${Uri.encodeComponent(karakter.anaRef)}&type=input', fit: BoxFit.cover, errorBuilder: (_, _, _) => const Icon(Icons.face, size: 24)) : const Icon(Icons.face, size: 24), ), const SizedBox(width: 6), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text(karakter.ad.isEmpty ? 'isimsiz' : karakter.ad, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.w700)), Text([ karakter.cinsiyet, karakter.yas, if (karakter.kiyafetler.isNotEmpty) '👗${karakter.kiyafetler.length}', ].where((s) => s.isNotEmpty).join(' • '), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 9, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5))), if (egitim != null) Text(egitim, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 8.5, color: FikoRenkler.neonAmber)) else if (karakter.loraAdi.isNotEmpty) const Text('✓ LoRA eğitildi', style: TextStyle(fontSize: 8.5, color: FikoRenkler.neonMor)), ], ), ), Column(children: [ InkWell( onTap: () => _KarakterKutuphanesiAksiyon.duzenle(context, ref, karakter), child: const Padding(padding: EdgeInsets.all(2), child: Icon(Icons.edit, size: 14)), ), Tooltip( message: 'AI ile referans portreler üret (4 açı)', child: InkWell( onTap: () => KarakterServisi.aiOlustur(ref, karakter), child: const Padding(padding: EdgeInsets.all(2), child: Icon(Icons.auto_fix_high, size: 14, color: FikoRenkler.neonAmber)), ), ), Tooltip( message: 'LoRA eğit', child: InkWell( onTap: () => _KarakterKutuphanesiAksiyon.egitMenu(context, ref, karakter), child: const Padding(padding: EdgeInsets.all(2), child: Icon(Icons.model_training, size: 15, color: FikoRenkler.neonMor)), ), ), ]), ]), ), ); } } final _comfyUrlKK = Provider((ref) => ref.watch(ayarlarProvider).comfyUrl); void _duzenle(BuildContext context, WidgetRef ref, Karakter k) => _KarakterKutuphanesiAksiyon.duzenle(context, ref, k); abstract final class _KarakterKutuphanesiAksiyon { static void egitMenu(BuildContext context, WidgetRef ref, Karakter k) { final refSayi = (k.anaRef.isNotEmpty ? 1 : 0) + k.refGorseller.length; showDialog(context: context, builder: (ctx) => AlertDialog( title: Text('${k.ad} — LoRA Eğit'), content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('$refSayi referans görsel ile LoRA eğitilecek.', style: const TextStyle(fontSize: 12)), if (refSayi < 3) const Padding(padding: EdgeInsets.only(top: 6), child: Text('⚠ En az 3-5 referans önerilir (Düzenle → referans ekle)', style: TextStyle(fontSize: 10.5, color: FikoRenkler.neonAmber))), const SizedBox(height: 10), for (final preset in loraEgitimPresetleri) ListTile( dense: true, contentPadding: EdgeInsets.zero, title: Text(preset.etiket, style: const TextStyle(fontSize: 12.5)), subtitle: Text('rank ${preset.rank} • ${preset.steps} adım', style: const TextStyle(fontSize: 10)), onTap: () { Navigator.pop(ctx); KarakterServisi.loraEgit(ref, k, preset); }, ), ]), actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Vazgeç'))], )); } static void duzenle(BuildContext context, WidgetRef ref, Karakter k) { final ad = TextEditingController(text: k.ad); final sac = TextEditingController(text: k.sacRengi); final goz = TextEditingController(text: k.gozRengi); final tetik = TextEditingController(text: k.tetik); final aciklama = TextEditingController(text: k.aciklama); var cinsiyet = k.cinsiyet, yas = k.yas, vucut = k.vucut, stil = k.stil; var yontem = k.identityYontem; final refler = [ if (k.anaRef.isNotEmpty) k.anaRef, ...k.refGorseller, ]; final kiyafetler = [...k.kiyafetler]; final url = ref.read(_comfyUrlKK); showDialog(context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setSt) => AlertDialog( title: Text(k.id == Isar_autoIncrement ? 'Yeni Karakter' : 'Karakter Düzenle'), content: SizedBox(width: 440, child: SingleChildScrollView( child: Column(mainAxisSize: MainAxisSize.min, children: [ TextField(controller: ad, decoration: const InputDecoration(labelText: 'İsim *')), const SizedBox(height: 8), Row(children: [ Expanded(child: _drop('Cinsiyet', _cinsiyetler, cinsiyet, (v) => setSt(() => cinsiyet = v))), const SizedBox(width: 8), Expanded(child: _drop('Yaş', _yaslar, yas, (v) => setSt(() => yas = v))), ]), const SizedBox(height: 8), Row(children: [ Expanded(child: TextField(controller: sac, decoration: const InputDecoration(labelText: 'Saç rengi'))), const SizedBox(width: 8), Expanded(child: TextField(controller: goz, decoration: const InputDecoration(labelText: 'Göz rengi'))), ]), const SizedBox(height: 8), Row(children: [ Expanded(child: _drop('Vücut', _vucutlar, vucut, (v) => setSt(() => vucut = v))), const SizedBox(width: 8), Expanded(child: _drop('Stil', _stiller, stil, (v) => setSt(() => stil = v))), ]), const SizedBox(height: 8), TextField(controller: aciklama, maxLines: 2, decoration: const InputDecoration(labelText: 'Serbest betimleme (İngilizce)')), const SizedBox(height: 8), TextField(controller: tetik, decoration: const InputDecoration( labelText: 'Tetik kelime (LoRA için)', hintText: 'örn: aysel_woman')), const SizedBox(height: 8), DropdownButtonFormField( initialValue: yontem, decoration: const InputDecoration(labelText: 'Kimlik yöntemi'), items: const [ DropdownMenuItem(value: IdentityYontem.pulid, child: Text('PuLID (yüz)')), DropdownMenuItem(value: IdentityYontem.instantid, child: Text('InstantID')), DropdownMenuItem(value: IdentityYontem.ipadapter, child: Text('IPAdapter')), DropdownMenuItem(value: IdentityYontem.lora, child: Text('LoRA (eğitilmiş)')), DropdownMenuItem(value: IdentityYontem.reactor, child: Text('Reactor (yüz değiştir)')), ], onChanged: (v) => setSt(() => yontem = v ?? yontem), ), const SizedBox(height: 10), Align(alignment: Alignment.centerLeft, child: Row(children: [ const Text('Referans görseller (yüz + LoRA eğitimi)', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700)), const Spacer(), Text('${refler.length}', style: const TextStyle(fontSize: 11, color: FikoRenkler.neonYesil)), ])), const SizedBox(height: 4), Wrap(spacing: 6, runSpacing: 6, children: [ for (var i = 0; i < refler.length; i++) Stack(children: [ Container(width: 56, height: 56, decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), border: Border.all(color: i == 0 ? FikoRenkler.neonYesil : Theme.of(ctx).dividerColor)), clipBehavior: Clip.antiAlias, child: Image.network('$url/view?filename=${Uri.encodeComponent(refler[i])}&type=input', fit: BoxFit.cover, errorBuilder: (_, _, _) => const Icon(Icons.image, size: 20))), Positioned(top: 0, right: 0, child: GestureDetector( onTap: () => setSt(() => refler.removeAt(i)), child: Container(color: Colors.black54, child: const Icon(Icons.close, size: 13, color: Colors.white)))), if (i == 0) const Positioned(bottom: 0, left: 0, child: Padding( padding: EdgeInsets.all(1), child: Text('ana', style: TextStyle(fontSize: 7, color: FikoRenkler.neonYesil)))), ]), GestureDetector( onTap: () async { final s = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: true, dialogTitle: 'Referans görsel(ler)'); if (s == null) return; for (final f in s.files) { if (f.path != null && File(f.path!).existsSync()) { final ad = await ref.read(comfyIstemciProvider).resimYukle(f.path!); refler.add(ad); } } setSt(() {}); }, child: Container(width: 56, height: 56, decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), border: Border.all(color: Theme.of(ctx).dividerColor)), child: const Icon(Icons.add_a_photo, size: 20)), ), ]), const SizedBox(height: 10), Align(alignment: Alignment.centerLeft, child: Row(children: [ const Text('👗 Kıyafetler (IPAdapter referansı)', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700)), const Spacer(), Text('${kiyafetler.length}', style: const TextStyle(fontSize: 11, color: FikoRenkler.neonPembe)), ])), const SizedBox(height: 4), Wrap(spacing: 6, runSpacing: 6, children: [ for (var i = 0; i < kiyafetler.length; i++) Stack(children: [ Container(width: 56, height: 56, decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), border: Border.all( color: FikoRenkler.neonPembe.withValues(alpha: 0.5))), clipBehavior: Clip.antiAlias, child: Image.network( '$url/view?filename=${Uri.encodeComponent(kiyafetler[i])}&type=input', fit: BoxFit.cover, errorBuilder: (_, _, _) => const Icon(Icons.checkroom, size: 20))), Positioned(top: 0, right: 0, child: GestureDetector( onTap: () => setSt(() => kiyafetler.removeAt(i)), child: Container(color: Colors.black54, child: const Icon(Icons.close, size: 13, color: Colors.white)))), ]), GestureDetector( onTap: () async { final s = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: true, dialogTitle: 'Kıyafet görsel(ler)i'); if (s == null) return; for (final f in s.files) { if (f.path != null && File(f.path!).existsSync()) { final ad = await ref.read(comfyIstemciProvider).resimYukle(f.path!); kiyafetler.add(ad); } } setSt(() {}); }, child: Container(width: 56, height: 56, decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), border: Border.all(color: Theme.of(ctx).dividerColor)), child: const Icon(Icons.checkroom, size: 20)), ), ]), ]), )), actions: [ if (k.id != Isar_autoIncrement) TextButton( onPressed: () { KarakterServisi.sil(k.id); Navigator.pop(ctx); }, child: const Text('Sil', style: TextStyle(color: Colors.redAccent))), TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Vazgeç')), FilledButton(onPressed: () { if (ad.text.trim().isEmpty) return; k ..ad = ad.text.trim() ..cinsiyet = cinsiyet ..yas = yas ..sacRengi = sac.text.trim() ..gozRengi = goz.text.trim() ..vucut = vucut ..stil = stil ..aciklama = aciklama.text.trim() ..tetik = tetik.text.trim() ..identityYontem = yontem ..anaRef = refler.isNotEmpty ? refler.first : '' ..refGorseller = refler.length > 1 ? refler.sublist(1) : [] ..kiyafetler = kiyafetler; KarakterServisi.kaydet(k); Navigator.pop(ctx); }, child: const Text('Kaydet')), ], ), )); } static Widget _drop(String etiket, List opts, String secili, void Function(String) onChange) { final deger = opts.contains(secili) ? secili : null; return DropdownButtonFormField( initialValue: deger, isExpanded: true, decoration: InputDecoration(labelText: etiket, isDense: true), items: [for (final o in opts) DropdownMenuItem(value: o, child: Text(o, style: const TextStyle(fontSize: 12)))], onChanged: (v) { if (v != null) onChange(v); }, ); } } // Isar.autoIncrement sabiti (yeni kayıt tespiti) // ignore: constant_identifier_names const Isar_autoIncrement = -9223372036854775808;