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 '../comfy/workflow/muzik_workflow.dart'; import '../galeri/studyo_gecmis.dart'; import '../model/model_servisi.dart' show comfySamplerlarProvider, comfySchedulerlarProvider; import '../resim/resim_form_state.dart' show loraListesiProvider; import '../veri/modeller/uretim_kaydi.dart'; import 'muzik_form_state.dart'; import 'muzik_kataloglari.dart'; import 'muzik_sihirbazi.dart'; import 'seslendirme_dialog.dart'; import 'soz_editoru.dart'; final muzikGecmisAcikProvider = StateProvider((ref) => false); class MuzikEkrani extends ConsumerWidget { const MuzikEkrani({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(muzikFormProvider); 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( flex: 11, child: Card( child: Padding( padding: const EdgeInsets.all(10), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const _PromptAlani(), const SizedBox(height: 8), const _KalitePresetSatiri(), const SizedBox(height: 12), const _TemelParametreler(), if (form.gelismisGorunum) ...[ const SizedBox(height: 12), const _GelismisParametreler(), ], ], ), ), ), ), ), const SizedBox(width: 14), Expanded( flex: 8, child: Card( child: Padding( padding: const EdgeInsets.all(10), child: const SozEditoru(), ), ), ), if (ref.watch(muzikGecmisAcikProvider)) ...[ const SizedBox(width: 14), const StudyoGecmisPaneli(tip: UretimTipi.muzik), ], ], ), ), const SizedBox(height: 12), const KuyrukPaneli(tipFiltresi: UretimTipi.muzik), ], ), ); } } class _UstBar extends ConsumerWidget { const _UstBar(); @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(muzikFormProvider); final notifier = ref.read(muzikFormProvider.notifier); return FikoUstBar( ikon: Icons.music_note_rounded, baslik: 'Müzik Stüdyosu', renk: FikoRenkler.neonCyan, solEylemler: [ _GorunumSecici( gelismis: form.gelismisGorunum, onDegisti: notifier.gorunumDegistir, ), ], eylemler: [ IconButton( tooltip: 'Prompt Sihirbazı', icon: const Icon(Icons.auto_fix_high, color: FikoRenkler.neonMor), onPressed: () => sihirbazDialogAc( context, baslik: 'Müzik Prompt Sihirbazı', ikon: Icons.auto_fix_high, renk: FikoRenkler.neonCyan, icerik: const MuzikSihirbazi(), ), ), IconButton( tooltip: 'Üretim geçmişi', icon: Icon(Icons.history, color: ref.watch(muzikGecmisAcikProvider) ? FikoRenkler.neonMor : null), onPressed: () => ref.read(muzikGecmisAcikProvider.notifier).update((v) => !v), ), IconButton( tooltip: 'Seslendirme & Ses Klonlama (TTS)', icon: const Icon(Icons.record_voice_over, color: FikoRenkler.neonCyan), onPressed: () => showSeslendirmeDialog(context, ref), ), const _SablonMenusu(), const SizedBox(width: 10), const _UretButonu(), ], ); } } class _GorunumSecici extends StatelessWidget { const _GorunumSecici({required this.gelismis, required this.onDegisti}); final bool gelismis; final ValueChanged onDegisti; @override Widget build(BuildContext context) { return 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: {gelismis}, onSelectionChanged: (s) => onDegisti(s.first), ); } } class _UretButonu extends ConsumerWidget { const _UretButonu(); @override Widget build(BuildContext context, WidgetRef ref) { return FikoUretButonu( etiket: 'Üret', onBas: () async { await ref.read(muzikFormProvider.notifier).uret(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Üretim kuyruğa eklendi'), duration: Duration(seconds: 2), )); } }, ); } } class _SablonMenusu extends ConsumerWidget { const _SablonMenusu(); @override Widget build(BuildContext context, WidgetRef ref) { final sablonlar = ref.watch(muzikSablonlariProvider).valueOrNull ?? []; final notifier = ref.read(muzikFormProvider.notifier); return MenuAnchor( builder: (context, ctrl, _) => IconButton( tooltip: 'Şablonlar', icon: const Icon(Icons.bookmarks_outlined), onPressed: () => ctrl.isOpen ? ctrl.close() : ctrl.open(), ), menuChildren: [ MenuItemButton( leadingIcon: const Icon(Icons.add, size: 17), onPressed: () => _kaydetDialog(context, notifier), child: const Text('Mevcut ayarları şablon olarak kaydet'), ), if (sablonlar.isNotEmpty) const Divider(), for (final s in sablonlar) MenuItemButton( leadingIcon: const Icon(Icons.music_note, size: 17), trailingIcon: IconButton( icon: const Icon(Icons.delete_outline, size: 16), onPressed: () => notifier.sablonSil(s.id), ), onPressed: () => notifier.sablonYukle(s), child: Text(s.ad), ), ], ); } void _kaydetDialog(BuildContext context, MuzikFormNotifier notifier) { final adCtrl = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Şablon Kaydet'), content: TextField( controller: adCtrl, autofocus: true, decoration: const InputDecoration(labelText: 'Şablon adı'), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Vazgeç')), FilledButton( onPressed: () { if (adCtrl.text.trim().isNotEmpty) { notifier.sablonKaydet(adCtrl.text.trim(), ''); Navigator.pop(ctx); } }, child: const Text('Kaydet'), ), ], ), ); } } class _PromptAlani extends ConsumerWidget { const _PromptAlani(); @override Widget build(BuildContext context, WidgetRef ref) { final tags = ref.watch(muzikFormProvider.select((s) => s.parametreler.tags)); final notifier = ref.read(muzikFormProvider.notifier); return TextFormField( key: ValueKey(tags), initialValue: tags, maxLines: 3, style: const TextStyle(fontSize: 12.5), decoration: const InputDecoration( labelText: 'Müzik Promptu (tags)', hintText: 'female vocals, turkish pop, energetic, synth pop, 120 bpm...', helperText: 'Sihirbaz seçimleri buraya otomatik derlenir; elle de düzenleyebilirsiniz', helperMaxLines: 2, ), onChanged: (v) { final p = ref.read(muzikFormProvider).parametreler; notifier.parametreGuncelle(p.kopyala(tags: v)); }, ); } } /// Kalite ön ayar çipleri (Hızlı/Dengeli/Kalite/Sinematik). class _KalitePresetSatiri extends ConsumerWidget { const _KalitePresetSatiri(); @override Widget build(BuildContext context, WidgetRef ref) { final p = ref.watch(muzikFormProvider.select((s) => s.parametreler)); final notifier = ref.read(muzikFormProvider.notifier); bool aktif(KalitePreseti k) => p.steps == k.steps && p.cfg == k.cfg && p.samplerName == k.sampler; return Row( children: [ const Icon(Icons.tune, size: 14, color: FikoRenkler.neonAmber), const SizedBox(width: 6), const Text('Kalite:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), const SizedBox(width: 8), Expanded( child: Wrap( spacing: 5, runSpacing: 5, children: [ for (final k in MuzikKataloglari.kalitePresetleri) GestureDetector( onTap: () => notifier.kalitePresetiUygula(k), child: AnimatedContainer( duration: const Duration(milliseconds: 120), padding: const EdgeInsets.symmetric( horizontal: 9, vertical: 4), decoration: BoxDecoration( color: aktif(k) ? FikoRenkler.neonAmber.withValues(alpha: 0.22) : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(7), border: Border.all( color: aktif(k) ? FikoRenkler.neonAmber : Theme.of(context).dividerColor), ), child: Text(k.etiket, style: TextStyle( fontSize: 11.5, fontWeight: aktif(k) ? FontWeight.w700 : FontWeight.w500, color: aktif(k) ? FikoRenkler.neonAmber : Theme.of(context).colorScheme.onSurface)), ), ), ], ), ), ], ); } } class _TemelParametreler extends ConsumerWidget { const _TemelParametreler(); @override Widget build(BuildContext context, WidgetRef ref) { final form = ref.watch(muzikFormProvider); final p = form.parametreler; final notifier = ref.read(muzikFormProvider.notifier); final modeller = ref.watch(aceModelleriProvider).valueOrNull ?? [p.model]; void guncelle(MuzikParametreleri yeni) => notifier.parametreGuncelle(yeni); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Expanded( flex: 3, child: DropdownButtonFormField( isExpanded: true, initialValue: modeller.isEmpty ? p.model : (modeller.contains(p.model) ? p.model : modeller.first), decoration: const InputDecoration(labelText: 'Model'), items: [ for (final m in (modeller.isEmpty ? [p.model] : modeller)) DropdownMenuItem( value: m, child: Text(m, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12))), ], onChanged: (v) { if (v == null) return; final j = {...p.toJson(), 'model': v}; // Turbo↔base geçişinde cfg/adım otomatik uyarlanır. Turbo // distilled → cfg 1.0 (CFG kapalı, az adım); base → cfg 4.0. final yeniTurbo = v.toLowerCase().contains('turbo'); final eskiTurbo = p.model.toLowerCase().contains('turbo'); if (yeniTurbo != eskiTurbo) { if (yeniTurbo) { j['steps'] = 8; j['cfg'] = 1.0; j['cfgScale'] = 1.0; } else { // base/XL de ACE-Step'te düşük cfg ister (yüksek = bozulma). j['steps'] = 50; j['cfg'] = 2.0; j['cfgScale'] = 1.0; } } guncelle(MuzikParametreleri.fromJson(j)); }, ), ), const SizedBox(width: 10), Expanded( flex: 2, child: DropdownButtonFormField( initialValue: MuzikKataloglari.diller.any((d) => d.id == p.dil) ? p.dil : MuzikKataloglari.diller.first.id, decoration: const InputDecoration(labelText: 'Dil'), items: [ for (final d in MuzikKataloglari.diller) DropdownMenuItem(value: d.id, child: Text(d.etiketTr)), ], onChanged: (v) { if (v != null) { guncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'dil': v})); } }, ), ), const SizedBox(width: 10), Expanded( flex: 2, child: DropdownButtonFormField( // ComfyUI 'keyscale' formatı (ör. 'E minor') bekler; değer // promptEn olmalı. Eşleşmezse ilk seçeneğe düş (crash önleme). initialValue: MuzikKataloglari.tonAnahtarlari .any((t) => t.promptEn == p.tonAnahtari) ? p.tonAnahtari : MuzikKataloglari.tonAnahtarlari.first.promptEn, decoration: const InputDecoration(labelText: 'Ton'), items: [ for (final t in MuzikKataloglari.tonAnahtarlari) DropdownMenuItem(value: t.promptEn, child: Text(t.etiketTr)), ], onChanged: (v) { if (v != null) { guncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'tonAnahtari': v})); } }, ), ), ], ), const SizedBox(height: 8), DropdownButtonFormField( isExpanded: true, initialValue: null, decoration: const InputDecoration( labelText: 'Makam (Türk müziği — ton + stil)', isDense: true, ), hint: const Text('Makam seç (opsiyonel)', style: TextStyle(fontSize: 11.5)), items: [ for (final m in MuzikKataloglari.makamlar) DropdownMenuItem( value: m.id, child: Text('${m.ad} · ${m.keyscale}', style: const TextStyle(fontSize: 11.5)), ), ], onChanged: (id) { if (id == null) return; final m = MuzikKataloglari.makamlar.firstWhere((x) => x.id == id); final ilkKelime = m.tag.split(',').first.trim().toLowerCase(); final yeniTag = p.tags.toLowerCase().contains(ilkKelime) ? p.tags : (p.tags.trim().isEmpty ? m.tag : '${p.tags.trim()}, ${m.tag}'); guncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'tonAnahtari': m.keyscale, 'tags': yeniTag})); }, ), if (!p.aioMu) _BilesenModelSeciciler(p: p, guncelle: guncelle), const SizedBox(height: 10), Row( children: [ const SizedBox( width: 78, child: Text('Süre', style: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w600))), Expanded( child: Slider( value: p.sureSn.clamp(5, 600), min: 5, max: 600, onChanged: (v) => guncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'sureSn': v.roundToDouble()})), ), ), SizedBox( width: 70, child: TextFormField( key: ValueKey('muziksure_${p.sureSn.round()}'), initialValue: '${p.sureSn.round()}', keyboardType: TextInputType.number, textAlign: TextAlign.right, style: const TextStyle(fontSize: 11.5, color: FikoRenkler.neonCyan), decoration: const InputDecoration(isDense: true, suffixText: 'sn'), onFieldSubmitted: (v) { final s = double.tryParse(v.trim()); if (s != null && s > 0) { guncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'sureSn': s})); } }, ), ), ], ), 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, ), ), if (!form.rastgeleSeed) Expanded( child: TextFormField( key: ValueKey(p.seed), initialValue: p.seed.toString(), decoration: const InputDecoration(labelText: 'Seed'), keyboardType: TextInputType.number, style: const TextStyle(fontSize: 12.5), onChanged: (v) { final seed = int.tryParse(v); if (seed != null) { guncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'seed': seed})); } }, ), ), ], ), ], ); } } /// Bileşen (aio olmayan) modellerde CLIP + VAE seçimi + uyarı notu. class _BilesenModelSeciciler extends ConsumerWidget { const _BilesenModelSeciciler({required this.p, required this.guncelle}); final MuzikParametreleri p; final void Function(MuzikParametreleri) guncelle; @override Widget build(BuildContext context, WidgetRef ref) { final clipler = ref.watch(aceClipProvider).valueOrNull ?? [p.clipModel]; final vaeler = ref.watch(aceVaeProvider).valueOrNull ?? [p.vaeModel]; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6), decoration: BoxDecoration( color: FikoRenkler.neonAmber.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(7), border: Border.all( color: FikoRenkler.neonAmber.withValues(alpha: 0.4)), ), child: const Row( children: [ Icon(Icons.info_outline, size: 14, color: FikoRenkler.neonAmber), SizedBox(width: 6), Expanded( child: Text( 'Bileşen model: CLIP ve VAE ayrı yükleniyor. Bazı XL/SFT ' 'modelleri ACE node\'larıyla uyumsuz olabilir.', style: TextStyle(fontSize: 10.5), ), ), ], ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: _ModelDrop( etiket: 'CLIP 1 (0.6b)', deger: p.clipModel, secenekler: clipler, onSec: (v) => guncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'clipModel': v})), ), ), const SizedBox(width: 8), Expanded( child: _ModelDrop( etiket: 'CLIP 2 (4b)', deger: p.clipModel2, secenekler: clipler, onSec: (v) => guncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'clipModel2': v})), ), ), const SizedBox(width: 8), Expanded( child: _ModelDrop( etiket: 'Ses VAE', deger: p.vaeModel, secenekler: vaeler, onSec: (v) => guncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'vaeModel': v})), ), ), ], ), ], ); } } /// Kompakt model seçim dropdown'u — kısa dosya adı gösterimi. class _ModelDrop extends StatelessWidget { const _ModelDrop({ required this.etiket, required this.deger, required this.secenekler, required this.onSec, }); final String etiket; final String deger; final List secenekler; final ValueChanged onSec; @override Widget build(BuildContext context) { final liste = secenekler.contains(deger) ? secenekler : [deger, ...secenekler]; return DropdownButtonFormField( initialValue: liste.contains(deger) ? deger : liste.first, isExpanded: true, decoration: InputDecoration(labelText: etiket), items: [ for (final m in liste) DropdownMenuItem( value: m, child: Text(m.replaceAll('.safetensors', ''), overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)), ), ], onChanged: (v) { if (v != null) onSec(v); }, ); } } class _GelismisParametreler extends ConsumerWidget { const _GelismisParametreler(); @override Widget build(BuildContext context, WidgetRef ref) { final p = ref.watch(muzikFormProvider.select((s) => s.parametreler)); final notifier = ref.read(muzikFormProvider.notifier); // Sampler/scheduler listesi ComfyUI'den canlı (beta dahil hepsi); offline'da // katalog fallback'i. Kullanıcının değeri listede yoksa yine de gösterilir. final canliSampler = ref.watch(comfySamplerlarProvider).valueOrNull; final canliScheduler = ref.watch(comfySchedulerlarProvider).valueOrNull; final samplerlar = [ ...(canliSampler == null || canliSampler.isEmpty ? MuzikKataloglari.samplerlar : canliSampler) ]; if (!samplerlar.contains(p.samplerName)) samplerlar.insert(0, p.samplerName); final schedulerlar = [ ...(canliScheduler == null || canliScheduler.isEmpty ? MuzikKataloglari.schedulerlar : canliScheduler) ]; if (!schedulerlar.contains(p.scheduler)) { schedulerlar.insert(0, p.scheduler); } void sayi(String alan, num deger) => notifier.parametreGuncelle( MuzikParametreleri.fromJson({...p.toJson(), alan: deger})); 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) => sayi(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('Steps', 'steps', p.steps.toDouble(), 4, 60, 56, tam: true), slider('CFG (≤2 önerilir)', 'cfg', p.cfg, 1, 8, 70), slider('CFG Scale (LM)', 'cfgScale', p.cfgScale, 0.5, 3, 50), slider('Temperature', 'temperature', p.temperature, 0.1, 2, 95), slider('Top-p', 'topP', p.topP, 0.1, 1, 90), slider('Top-k', 'topK', p.topK.toDouble(), 0, 100, 100, tam: true), slider('Min-p', 'minP', p.minP, 0, 1, 100), slider('Shift', 'shift', p.shift, 0.5, 10, 95), slider('BPM', 'bpm', p.bpm.toDouble(), 50, 220, 170, tam: true), slider('Batch (varyant)', 'batch', p.batch.toDouble(), 1, 10, 9, tam: true), const SizedBox(height: 8), 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) { notifier.parametreGuncelle(MuzikParametreleri.fromJson( {...p.toJson(), '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) { notifier.parametreGuncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'scheduler': v})); } }, ), ), const SizedBox(width: 10), Expanded( child: DropdownButtonFormField( initialValue: MuzikKataloglari.mp3Kaliteleri.contains(p.mp3Kalite) ? p.mp3Kalite : MuzikKataloglari.mp3Kaliteleri.first, decoration: const InputDecoration(labelText: 'MP3 Kalite'), items: [ for (final q in MuzikKataloglari.mp3Kaliteleri) DropdownMenuItem( value: q, child: Text(q, style: const TextStyle(fontSize: 11.5))), ], onChanged: (v) { if (v != null) { notifier.parametreGuncelle(MuzikParametreleri.fromJson( {...p.toJson(), 'mp3Kalite': v})); } }, ), ), ], ), const Divider(height: 16), const _MuzikLoraPanel(), ], ); } } /// Müzik stili LoRA seçici — ekle/güç/sil (ACE-Step model-only LoRA'ları). class _MuzikLoraPanel extends ConsumerWidget { const _MuzikLoraPanel(); @override Widget build(BuildContext context, WidgetRef ref) { final loralar = ref.watch(muzikFormProvider.select((s) => s.parametreler.loralar)); final notifier = ref.read(muzikFormProvider.notifier); final tumLoralar = ref.watch(loraListesiProvider).valueOrNull ?? []; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ const Icon(Icons.library_music, size: 16, color: FikoRenkler.neonYesil), const SizedBox(width: 6), Text(loralar.isEmpty ? 'Müzik LoRA' : 'Müzik LoRA (${loralar.length})', style: const TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700)), const Spacer(), MenuAnchor( builder: (context, ctrl, _) => IconButton( visualDensity: VisualDensity.compact, tooltip: 'LoRA ekle', icon: const Icon(Icons.add_circle_outline, size: 18), onPressed: () => ctrl.isOpen ? ctrl.close() : ctrl.open(), ), menuChildren: [ if (tumLoralar.isEmpty) const MenuItemButton( onPressed: null, child: Text('LoRA bulunamadı')), for (final l in tumLoralar) MenuItemButton( onPressed: () => notifier.loraEkle(l), child: Text(l.replaceAll('.safetensors', ''), style: const TextStyle(fontSize: 12)), ), ], ), ]), for (var i = 0; i < loralar.length; i++) Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row(children: [ Expanded( flex: 3, child: Text(loralar[i].dosya.replaceAll('.safetensors', ''), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11.5)), ), Expanded( flex: 4, child: Slider( value: loralar[i].guc.clamp(-2, 2), min: -2, max: 2, divisions: 80, onChanged: (v) => notifier.loraGuc(i, v), ), ), SizedBox( width: 32, child: Text(loralar[i].guc.toStringAsFixed(2), style: const TextStyle(fontSize: 10.5)), ), IconButton( visualDensity: VisualDensity.compact, icon: const Icon(Icons.close, size: 15), onPressed: () => notifier.loraSil(i), ), ]), ), if (loralar.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Text( 'İndirdiğin ACE-Step LoRA\'larını + ile ekle (Modeller → ACE-Step LoRA)', style: TextStyle( fontSize: 10.5, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.5))), ), ], ); } }