import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' as p; import '../../durum/karakter_durumu.dart'; import '../../durum/model_yonetim_durumu.dart'; /// Model & LoRA yönetim sayfası — 3 sekme: Görsel, Ses, Video. class ModelYonetimSayfasi extends ConsumerStatefulWidget { const ModelYonetimSayfasi({super.key}); @override ConsumerState createState() => _ModelYonetimSayfasiState(); } class _ModelYonetimSayfasiState extends ConsumerState with SingleTickerProviderStateMixin { late final TabController _tabKontrol; @override void initState() { super.initState(); _tabKontrol = TabController(length: 3, vsync: this); } @override void dispose() { _tabKontrol.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( body: Column( children: [ Container( decoration: BoxDecoration( color: theme.colorScheme.surface, border: Border(bottom: BorderSide(color: theme.dividerColor)), ), child: Row( children: [ const SizedBox(width: 12), Icon(Icons.inventory_2, color: theme.colorScheme.primary), const SizedBox(width: 8), Text('Model Yönetimi', style: theme.textTheme.titleMedium), const SizedBox(width: 16), Expanded( child: TabBar( controller: _tabKontrol, isScrollable: true, tabAlignment: TabAlignment.start, tabs: const [ Tab( icon: Icon(Icons.image_outlined), text: 'Görsel (SDXL)'), Tab( icon: Icon(Icons.music_note_outlined), text: 'Ses (ACE + RVC)'), Tab( icon: Icon(Icons.movie_creation_outlined), text: 'Video'), ], ), ), IconButton( tooltip: 'Yenile', icon: const Icon(Icons.refresh), onPressed: () => ref.read(modelYonetimProvider.notifier).yenile(), ), const SizedBox(width: 8), ], ), ), Expanded( child: TabBarView( controller: _tabKontrol, children: const [ _GorselSekmesi(), _SesSekmesi(), _VideoSekmesi(), ], ), ), ], ), ); } } // ============ SEKMELER ============ class _GorselSekmesi extends ConsumerWidget { const _GorselSekmesi(); @override Widget build(BuildContext context, WidgetRef ref) { final liste = ref.watch(gorselModellerProvider); return _SekmeIcerik( kategori: 'gorsel', tema: Colors.amber, bos: 'Henüz görsel modeli yok — sağ üstten import et.', liste: liste, tipler: const ['SDXL LoRA', 'SDXL Base'], ); } } class _SesSekmesi extends ConsumerWidget { const _SesSekmesi(); @override Widget build(BuildContext context, WidgetRef ref) { final ace = ref.watch(aceModellerProvider); final rvc = ref.watch(rvcModellerProvider); return _SekmeIcerik( kategori: 'ses', tema: Colors.teal, bos: 'Henüz ses modeli yok — sağ üstten import et.', liste: [...ace, ...rvc], tipler: const ['ACE LoRA', 'RVC Model'], ); } } class _VideoSekmesi extends StatelessWidget { const _VideoSekmesi(); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.movie_creation_outlined, size: 48, color: Colors.red.shade300), const SizedBox(height: 12), Text('Video Modelleri', style: theme.textTheme.titleMedium), const SizedBox(height: 4), Text( 'Wan / LTX import desteği henüz eklenmedi.', style: theme.textTheme.bodySmall, ), ], ), ), ); } } // ============ ORTAK İÇERİK ============ class _SekmeIcerik extends ConsumerWidget { final String kategori; final Color tema; final String bos; final List liste; final List tipler; const _SekmeIcerik({ required this.kategori, required this.tema, required this.bos, required this.liste, required this.tipler, }); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return Column( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), color: tema.withValues(alpha: 0.06), child: Row( children: [ Text('${liste.length} model', style: theme.textTheme.labelLarge), const Spacer(), OutlinedButton.icon( icon: const Icon(Icons.cloud_download_outlined, size: 18), label: const Text('URL\'den İmport Et'), onPressed: () => _urlDialogAc(context, ref, kategori), ), const SizedBox(width: 8), FilledButton.tonalIcon( icon: const Icon(Icons.upload_file, size: 18), label: const Text('İmport Et'), style: FilledButton.styleFrom(backgroundColor: tema), onPressed: () => _importDialogAc(context, ref, kategori, tipler), ), ], ), ), Expanded( child: liste.isEmpty ? Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.inbox_outlined, size: 48, color: theme.disabledColor), const SizedBox(height: 8), Text(bos, style: theme.textTheme.bodyMedium), ], ), ), ) : ListView.separated( padding: const EdgeInsets.all(12), itemCount: liste.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (ctx, i) => _ModelKart(model: liste[i], tema: tema), ), ), ], ); } } class _ModelKart extends ConsumerWidget { final ModelKaydi model; final Color tema; const _ModelKart({required this.model, required this.tema}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final tarih = _tarihFormatla(model.tarih); return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHigh, border: Border.all(color: theme.dividerColor), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: tema.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), ), child: Icon(_ikonTipe(model.tip), color: tema), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: Text( model.isim, style: theme.textTheme.titleSmall, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: tema.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(4), ), child: Text(model.tipEtiketi, style: theme.textTheme.labelSmall), ), ], ), const SizedBox(height: 2), Text( '${model.boyutMb.toStringAsFixed(1)} MB · $tarih' '${model.tetikleyici.isNotEmpty ? " · ${model.tetikleyici}" : ""}' '${model.karakterId != null ? " · karakter atanmış" : ""}', style: theme.textTheme.bodySmall, overflow: TextOverflow.ellipsis, ), ], ), ), IconButton( tooltip: 'Sil', icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 20), onPressed: () async { final onay = await showDialog( context: context, builder: (c) => AlertDialog( title: const Text('Modeli sil?'), content: Text( '"${model.isim}" kalıcı olarak silinecek. Onaylıyor musun?'), actions: [ TextButton( onPressed: () => Navigator.pop(c, false), child: const Text('İptal')), FilledButton.tonal( style: FilledButton.styleFrom( backgroundColor: Colors.red.shade100), onPressed: () => Navigator.pop(c, true), child: const Text('Sil')), ], ), ); if (onay == true) { final ok = await ref .read(modelYonetimProvider.notifier) .sil(model); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(ok ? 'Silindi.' : 'Silme başarısız.'), )); } } }, ), ], ), ); } String _tarihFormatla(DateTime t) { const aylar = [ 'Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'Ağu', 'Eyl', 'Eki', 'Kas', 'Ara' ]; return '${t.day.toString().padLeft(2, "0")} ${aylar[t.month - 1]} ${t.year}'; } IconData _ikonTipe(String tip) => switch (tip) { 'sdxl_lora' => Icons.auto_awesome, 'sdxl_base' => Icons.image, 'ace_lora' => Icons.queue_music, 'rvc' => Icons.record_voice_over, _ => Icons.inventory_2, }; } // ============ DOSYA İMPORT DIALOG ============ void _importDialogAc(BuildContext context, WidgetRef ref, String kategori, List tipler) { showDialog( context: context, builder: (c) => _ImportDialog(kategori: kategori, tipSecenekleri: tipler), ); } class _ImportDialog extends ConsumerStatefulWidget { final String kategori; final List tipSecenekleri; const _ImportDialog( {required this.kategori, required this.tipSecenekleri}); @override ConsumerState<_ImportDialog> createState() => _ImportDialogState(); } class _ImportDialogState extends ConsumerState<_ImportDialog> { String? _dosyaYolu; String? _indexYolu; // sadece RVC final _isim = TextEditingController(); final _tetikleyici = TextEditingController(); late String _seciliTip; String? _karakterId; String _cins = 'person'; bool _yukleniyor = false; double _ilerleme = 0; @override void initState() { super.initState(); _seciliTip = widget.tipSecenekleri.first; } @override void dispose() { _isim.dispose(); _tetikleyici.dispose(); super.dispose(); } Future _dosyaSec() async { final r = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: const ['safetensors', 'pth', 'ckpt', 'bin'], ); if (r != null && r.files.single.path != null) { setState(() { _dosyaYolu = r.files.single.path!; if (_isim.text.isEmpty) { _isim.text = p.basenameWithoutExtension(r.files.single.path!); } }); } } Future _indexSec() async { final r = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: const ['index'], ); if (r != null && r.files.single.path != null) { setState(() => _indexYolu = r.files.single.path!); } } Future _gonder() async { if (_dosyaYolu == null || _isim.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Dosya ve isim zorunlu.')), ); return; } setState(() { _yukleniyor = true; _ilerleme = 0; }); final servis = ref.read(modelYonetimProvider.notifier).servis; void onProgress(int g, int t) { if (t > 0) setState(() => _ilerleme = g / t); } bool ok = false; try { switch (_seciliTip) { case 'SDXL LoRA': ok = await servis.sdxlLoraImport( dosyaYolu: _dosyaYolu!, isim: _isim.text.trim(), karakterId: _karakterId, tetikleyici: _tetikleyici.text.trim(), cins: _cins, ilerleme: onProgress, ); break; case 'SDXL Base': ok = await servis.sdxlBaseImport( dosyaYolu: _dosyaYolu!, isim: _isim.text.trim(), ilerleme: onProgress, ); break; case 'ACE LoRA': ok = await servis.aceLoraImport( dosyaYolu: _dosyaYolu!, isim: _isim.text.trim(), karakterId: _karakterId, ilerleme: onProgress, ); break; case 'RVC Model': ok = await servis.rvcImport( pthYolu: _dosyaYolu!, indexYolu: _indexYolu, isim: _isim.text.trim(), karakterId: _karakterId, ilerleme: onProgress, ); break; } } finally { setState(() => _yukleniyor = false); } if (!mounted) return; if (ok) { await ref.read(modelYonetimProvider.notifier).yenile(); if (mounted) Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Model başarıyla içe aktarıldı.')), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('İmport başarısız oldu.')), ); } } @override Widget build(BuildContext context) { final karakterler = ref.watch(karakterlerProvider); final loraTipi = _seciliTip == 'SDXL LoRA' || _seciliTip == 'ACE LoRA'; final isRvc = _seciliTip == 'RVC Model'; return AlertDialog( title: Row( children: [ const Icon(Icons.upload_file), const SizedBox(width: 8), Text('İmport · ${widget.kategori == 'gorsel' ? 'Görsel' : 'Ses'}'), ], ), content: SizedBox( width: 480, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SegmentedButton( segments: widget.tipSecenekleri .map((t) => ButtonSegment(value: t, label: Text(t))) .toList(), selected: {_seciliTip}, onSelectionChanged: (s) => setState(() => _seciliTip = s.first), ), const SizedBox(height: 12), OutlinedButton.icon( icon: const Icon(Icons.attach_file), label: Text(_dosyaYolu != null ? p.basename(_dosyaYolu!) : 'Dosya seç (.safetensors / .pth / .ckpt)'), onPressed: _yukleniyor ? null : _dosyaSec, ), if (isRvc) ...[ const SizedBox(height: 8), OutlinedButton.icon( icon: const Icon(Icons.list_alt), label: Text(_indexYolu != null ? p.basename(_indexYolu!) : 'Index dosyası (.index, opsiyonel)'), onPressed: _yukleniyor ? null : _indexSec, ), ], const SizedBox(height: 12), TextField( controller: _isim, decoration: const InputDecoration( labelText: 'İsim', border: OutlineInputBorder(), ), ), if (loraTipi) ...[ const SizedBox(height: 12), TextField( controller: _tetikleyici, decoration: const InputDecoration( labelText: 'Tetikleyici (opsiyonel)', border: OutlineInputBorder(), ), ), ], if (_seciliTip == 'SDXL LoRA') ...[ const SizedBox(height: 12), DropdownButtonFormField( initialValue: _cins, decoration: const InputDecoration( labelText: 'Cins', border: OutlineInputBorder(), ), items: const [ DropdownMenuItem( value: 'person', child: Text('person')), DropdownMenuItem(value: 'woman', child: Text('woman')), DropdownMenuItem(value: 'man', child: Text('man')), ], onChanged: (v) => setState(() => _cins = v ?? 'person'), ), ], const SizedBox(height: 12), DropdownButtonFormField( initialValue: _karakterId, decoration: const InputDecoration( labelText: 'Karakter (opsiyonel)', border: OutlineInputBorder(), ), items: [ const DropdownMenuItem( value: null, child: Text('Atama yok')), ...karakterler.map((k) => DropdownMenuItem( value: k.id, child: Text(k.isim.isEmpty ? k.id : k.isim))), ], onChanged: (v) => setState(() => _karakterId = v), ), if (_yukleniyor) ...[ const SizedBox(height: 16), LinearProgressIndicator(value: _ilerleme), const SizedBox(height: 4), Text('Yükleniyor… %${(_ilerleme * 100).toStringAsFixed(0)}'), ], ], ), ), ), actions: [ TextButton( onPressed: _yukleniyor ? null : () => Navigator.pop(context), child: const Text('İptal'), ), FilledButton.icon( icon: const Icon(Icons.upload), label: const Text('İmport Et'), onPressed: _yukleniyor ? null : _gonder, ), ], ); } } // ============ URL İMPORT DIALOG ============ void _urlDialogAc(BuildContext context, WidgetRef ref, String kategori) { showDialog( context: context, builder: (c) => _UrlImportDialog(kategori: kategori), ); } class _UrlImportDialog extends ConsumerStatefulWidget { final String kategori; const _UrlImportDialog({required this.kategori}); @override ConsumerState<_UrlImportDialog> createState() => _UrlImportDialogState(); } class _UrlImportDialogState extends ConsumerState<_UrlImportDialog> { final _url = TextEditingController(); final _isim = TextEditingController(); String _tip = 'SDXL LoRA'; String? _karakterId; bool _yukleniyor = false; double _ilerleme = 0; @override void initState() { super.initState(); if (widget.kategori == 'ses') _tip = 'ACE LoRA'; } @override void dispose() { _url.dispose(); _isim.dispose(); super.dispose(); } Future _indir() async { if (_url.text.trim().isEmpty || _isim.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('URL ve isim zorunlu.')), ); return; } if (_tip != 'SDXL LoRA') { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'URL import şu an sadece SDXL LoRA için destekleniyor.')), ); return; } setState(() { _yukleniyor = true; _ilerleme = 0; }); final servis = ref.read(modelYonetimProvider.notifier).servis; final ok = await servis.sdxlLoraImportUrl( url: _url.text.trim(), isim: _isim.text.trim(), karakterId: _karakterId, ilerleme: (g, t) { if (t > 0) setState(() => _ilerleme = g / t); }, ); setState(() => _yukleniyor = false); if (!mounted) return; if (ok) { await ref.read(modelYonetimProvider.notifier).yenile(); if (mounted) Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('İndirildi ve içe aktarıldı.')), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('İndirme başarısız.')), ); } } @override Widget build(BuildContext context) { final karakterler = ref.watch(karakterlerProvider); final tipler = widget.kategori == 'gorsel' ? const ['SDXL LoRA', 'SDXL Base'] : const ['ACE LoRA', 'RVC Model']; return AlertDialog( title: Row( children: const [ Icon(Icons.cloud_download_outlined), SizedBox(width: 8), Text('URL\'den İmport'), ], ), content: SizedBox( width: 480, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: _url, decoration: const InputDecoration( labelText: 'URL', hintText: 'https://huggingface.co/.../lora.safetensors', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextField( controller: _isim, decoration: const InputDecoration( labelText: 'İsim', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), SegmentedButton( segments: tipler .map((t) => ButtonSegment(value: t, label: Text(t))) .toList(), selected: {_tip}, onSelectionChanged: (s) => setState(() => _tip = s.first), ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: _karakterId, decoration: const InputDecoration( labelText: 'Karakter (opsiyonel)', border: OutlineInputBorder(), ), items: [ const DropdownMenuItem( value: null, child: Text('Atama yok')), ...karakterler.map((k) => DropdownMenuItem( value: k.id, child: Text(k.isim.isEmpty ? k.id : k.isim))), ], onChanged: (v) => setState(() => _karakterId = v), ), if (_yukleniyor) ...[ const SizedBox(height: 16), LinearProgressIndicator( value: _ilerleme > 0 ? _ilerleme : null), const SizedBox(height: 4), Text(_ilerleme > 0 ? 'İndiriliyor… %${(_ilerleme * 100).toStringAsFixed(0)}' : 'İndiriliyor…'), ], ], ), ), actions: [ TextButton( onPressed: _yukleniyor ? null : () => Navigator.pop(context), child: const Text('İptal')), FilledButton.icon( icon: const Icon(Icons.cloud_download), label: const Text('İndir + İmport'), onPressed: _yukleniyor ? null : _indir, ), ], ); } }