import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../cekirdek/muzik_kataloglari.dart'; import '../../durum/ace_step_sablon_durumu.dart'; /// Sihirbazda kullanıcı seçimleri. class SihirbazSecimler { final Set kulturler; final Set turler; final Set moodlar; final int bpm; final int enerji; // 0-100 final String cinsiyet; // erkek/kadın/... final Set vokalKarakterler; final Set enstrumanlar; final String? ekstraNot; // serbest açıklama final double yaraticilik; // 0-1 (LM temperature ile eşlenir) const SihirbazSecimler({ this.kulturler = const {}, this.turler = const {}, this.moodlar = const {}, this.bpm = 110, this.enerji = 60, this.cinsiyet = 'erkek', this.vokalKarakterler = const {}, this.enstrumanlar = const {}, this.ekstraNot, this.yaraticilik = 0.85, }); Map toJson() => { 'kulturler': kulturler.toList(), 'turler': turler.toList(), 'moodlar': moodlar.toList(), 'bpm': bpm, 'enerji': enerji, 'cinsiyet': cinsiyet, 'vokalKarakterler': vokalKarakterler.toList(), 'enstrumanlar': enstrumanlar.toList(), 'ekstraNot': ekstraNot, 'yaraticilik': yaraticilik, }; static SihirbazSecimler fromJson(Map j) => SihirbazSecimler( kulturler: ((j['kulturler'] as List?) ?? []).cast().toSet(), turler: ((j['turler'] as List?) ?? []).cast().toSet(), moodlar: ((j['moodlar'] as List?) ?? []).cast().toSet(), bpm: (j['bpm'] as num?)?.toInt() ?? 110, enerji: (j['enerji'] as num?)?.toInt() ?? 60, cinsiyet: j['cinsiyet'] as String? ?? 'erkek', vokalKarakterler: ((j['vokalKarakterler'] as List?) ?? []).cast().toSet(), enstrumanlar: ((j['enstrumanlar'] as List?) ?? []).cast().toSet(), ekstraNot: j['ekstraNot'] as String?, yaraticilik: (j['yaraticilik'] as num?)?.toDouble() ?? 0.85, ); } /// Sihirbaz çıktısı — form'u dolduracak alanlar. class SihirbazSonuc { final String prompt; final String negatifPrompt; final int bpm; final String dil; final String cinsiyet; final double lmTemperature; final double cfg; final SihirbazSecimler secimler; final String onerilenLyricsTaslak; // boş bile olabilir const SihirbazSonuc({ required this.prompt, required this.negatifPrompt, required this.bpm, required this.dil, required this.cinsiyet, required this.lmTemperature, required this.cfg, required this.secimler, required this.onerilenLyricsTaslak, }); } /// Sihirbaz dialog'u. Kullanıcı seçim yapar, çıktıyı [SihirbazSonuc] olarak döner. class AceStepSihirbazDialog extends ConsumerStatefulWidget { final SihirbazSecimler? baslangic; const AceStepSihirbazDialog({super.key, this.baslangic}); @override ConsumerState createState() => _AceStepSihirbazDialogState(); } class _AceStepSihirbazDialogState extends ConsumerState { late Set _kulturler; late Set _turler; late Set _moodlar; late int _bpm; late int _enerji; late String _cinsiyet; late Set _vokal; late Set _enstrumanlar; late TextEditingController _notCtrl; late double _yaraticilik; @override void initState() { super.initState(); final b = widget.baslangic; _kulturler = {...?b?.kulturler}; _turler = {...?b?.turler}; _moodlar = {...?b?.moodlar}; _bpm = b?.bpm ?? 110; _enerji = b?.enerji ?? 60; _cinsiyet = b?.cinsiyet ?? 'erkek'; _vokal = {...?b?.vokalKarakterler}; _enstrumanlar = {...?b?.enstrumanlar}; _notCtrl = TextEditingController(text: b?.ekstraNot ?? ''); _yaraticilik = b?.yaraticilik ?? 0.85; } @override void dispose() { _notCtrl.dispose(); super.dispose(); } SihirbazSecimler _mevcutSecimler() => SihirbazSecimler( kulturler: _kulturler, turler: _turler, moodlar: _moodlar, bpm: _bpm, enerji: _enerji, cinsiyet: _cinsiyet, vokalKarakterler: _vokal, enstrumanlar: _enstrumanlar, ekstraNot: _notCtrl.text.trim().isEmpty ? null : _notCtrl.text.trim(), yaraticilik: _yaraticilik, ); /// Seçimlerden prompt + negatif + diğer değerleri üretir. SihirbazSonuc _sonucHesapla() { final s = _mevcutSecimler(); // Kültürler → primary dil + tarz açıklamaları final kulturEtiket = MuzikKatalog.kulturler .where((k) => s.kulturler.contains(k.kod)) .map((k) => k.aciklama) .toList(); final birincilKultur = MuzikKatalog.kulturler .where((k) => s.kulturler.contains(k.kod)) .firstOrNull; final dil = birincilKultur?.kod ?? 'en'; // Türler → genre listesi (İngilizce aciklama prompt için) final turEtiket = MuzikKatalog.turler .where((t) => s.turler.contains(t.kod)) .map((t) => t.aciklama) .toList(); // Moodlar final moodEtiket = MuzikKatalog.moodlar .where((m) => s.moodlar.contains(m.kod)) .map((m) => m.aciklama) .toList(); // Vokal karakterleri final vokalEtiket = MuzikKatalog.vokalKarakterleri .where((v) => s.vokalKarakterler.contains(v.kod)) .map((v) => v.aciklama) // İngilizce — prompt için .toList(); // Enstrümanlar — kod (acoustic_guitar) → "acoustic guitar" English final enstrEtiket = MuzikKatalog.enstrumanlar .where((e) => s.enstrumanlar.contains(e.kod)) .map((e) => e.kod.replaceAll('_', ' ')) .toList(); // Tempo etiketi — English (ACE-Step prompt İngilizce olmalı) String bpmEnglish(int bpm) { if (bpm < 70) return 'very slow tempo (largo)'; if (bpm < 90) return 'slow tempo (adagio)'; if (bpm < 110) return 'moderate tempo (andante)'; if (bpm < 130) return 'medium tempo (moderato)'; if (bpm < 160) return 'fast tempo (allegro)'; if (bpm < 190) return 'very fast tempo (presto)'; return 'extremely fast tempo (prestissimo)'; } String enerjiEnglish(int v) { if (v < 20) return 'low energy'; if (v < 40) return 'medium-low energy'; if (v < 60) return 'medium energy'; if (v < 80) return 'high energy'; return 'very high energy'; } // Prompt cümlesi — pure English for ACE-Step final parcalar = []; if (turEtiket.isNotEmpty) { parcalar.add(turEtiket.length > 1 ? 'fusion of ${turEtiket.join(' and ')}' : turEtiket.first); } if (kulturEtiket.isNotEmpty) { parcalar.add(kulturEtiket.join(', ')); } if (moodEtiket.isNotEmpty) { parcalar.add(moodEtiket.join(', ')); } parcalar.add('${bpmEnglish(s.bpm)} (${s.bpm} BPM)'); parcalar.add(enerjiEnglish(s.enerji)); parcalar.add(MuzikKatalog.cinsiyetIngilizce(s.cinsiyet)); if (vokalEtiket.isNotEmpty) { parcalar.add('vocal style: ${vokalEtiket.join(', ')}'); } if (enstrEtiket.isNotEmpty) { parcalar.add( 'featured instruments: ${enstrEtiket.join(', ')} (prominent in mix)'); } if (s.ekstraNot != null) { parcalar.add(s.ekstraNot!); } final prompt = parcalar.join(', '); // Negative prompt — kalite + istenmeyen şeyler final negParcalar = [ 'low quality', 'muffled', 'distorted', 'noise', 'clipping', 'auto-tune artifacts', ]; if (s.cinsiyet == 'yok (enstrümantal)') { negParcalar.add('vocals'); } // Mood'a göre olumsuzlar if (s.moodlar.contains('energetic') || s.moodlar.contains('aggressive') || s.moodlar.contains('intense')) { negParcalar.add('boring, sleepy'); } if (s.moodlar.contains('calm') || s.moodlar.contains('peaceful') || s.moodlar.contains('romantic')) { negParcalar.add('aggressive, harsh'); } if (s.moodlar.contains('dark') || s.moodlar.contains('sad')) { negParcalar.add('cheerful, upbeat'); } if (s.moodlar.contains('uplifting') || s.moodlar.contains('hopeful')) { negParcalar.add('depressing, dark'); } final negatif = negParcalar.join(', '); // CFG: enerjiye göre final cfg = (5.5 + (s.enerji / 100) * 4).clamp(3.0, 9.5); // LM temperature: yaratıcılıktan final lmTemp = (s.yaraticilik * 1.6).clamp(0.4, 1.6); // Lyrics taslağı — boş bırak (kullanıcı doldursun) ama yapıyı öner final lyricsTaslak = s.cinsiyet == 'yok (enstrümantal)' ? '' : '[Verse 1]\n\n[Chorus]\n\n[Verse 2]\n\n[Chorus]\n\n[Bridge]\n\n[Outro]'; return SihirbazSonuc( prompt: prompt, negatifPrompt: negatif, bpm: s.bpm, dil: dil, cinsiyet: s.cinsiyet, lmTemperature: lmTemp, cfg: cfg, secimler: s, onerilenLyricsTaslak: lyricsTaslak, ); } Future _sablonOlarakKaydet() async { final isimCtrl = TextEditingController(text: _onerilenIsim()); final aciklamaCtrl = TextEditingController(); final onay = await showDialog( context: context, useRootNavigator: true, builder: (ctx) => AlertDialog( title: const Text('Şablon olarak kaydet'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: isimCtrl, autofocus: true, decoration: const InputDecoration( labelText: 'Şablon adı', isDense: true, ), ), const SizedBox(height: 8), TextField( controller: aciklamaCtrl, maxLines: 2, decoration: const InputDecoration( labelText: 'Açıklama (opsiyonel)', isDense: true, ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('İptal')), FilledButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Kaydet')), ], ), ); if (onay == true && isimCtrl.text.trim().isNotEmpty) { final sonuc = _sonucHesapla(); // Form snapshot benzeri ayarları üret final ayarlar = { 'prompt': sonuc.prompt, 'negatifPrompt': sonuc.negatifPrompt, 'bpm': sonuc.bpm, 'dil': sonuc.dil, 'lmTemperature': sonuc.lmTemperature, 'cfg': sonuc.cfg, 'kullaniciCinsiyet': sonuc.cinsiyet, 'onerilenLyricsTaslak': sonuc.onerilenLyricsTaslak, }; await ref.read(aceStepSablonProvider.notifier).kaydet( isim: isimCtrl.text.trim(), aciklama: aciklamaCtrl.text.trim().isEmpty ? null : aciklamaCtrl.text.trim(), ayarlar: ayarlar, sihirbazSecimler: _mevcutSecimler().toJson(), ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('"${isimCtrl.text.trim()}" şablonu kaydedildi')), ); } } } String _onerilenIsim() { final ilkTur = MuzikKatalog.turler .where((t) => _turler.contains(t.kod)) .firstOrNull ?.etiket; final ilkKultur = MuzikKatalog.kulturler .where((k) => _kulturler.contains(k.kod)) .firstOrNull ?.etiket; final parcalar = [ if (ilkKultur != null) ilkKultur.replaceAll(RegExp(r'[^\w\s]'), '').trim(), if (ilkTur != null) ilkTur, '${_bpm}bpm', ]; return parcalar.join(' '); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final sonuc = _sonucHesapla(); return Dialog( insetPadding: const EdgeInsets.all(20), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 980, maxHeight: 800), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header Padding( padding: const EdgeInsets.fromLTRB(16, 12, 12, 8), child: Row( children: [ Icon(Icons.auto_fix_high, color: theme.colorScheme.primary, size: 24), const SizedBox(width: 8), Text('Müzik Üretim Sihirbazı', style: theme.textTheme.titleLarge), const Spacer(), IconButton( icon: const Icon(Icons.bookmark_add_outlined), tooltip: 'Bu seçimleri şablon olarak kaydet', onPressed: _sablonOlarakKaydet, ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), ], ), ), const Divider(height: 1), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Sol: seçim alanları Expanded( flex: 3, child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _Baslik( 'Kültürler / Ülkeler', 'Birden fazla seçebilirsin → karma stil. İlki birincil dil olur.', ), Wrap( spacing: 6, runSpacing: 6, children: MuzikKatalog.kulturler.map((k) { final aktif = _kulturler.contains(k.kod); return FilterChip( label: Text(k.etiket, style: const TextStyle(fontSize: 12)), selected: aktif, onSelected: (v) => setState(() { if (v) { _kulturler.add(k.kod); } else { _kulturler.remove(k.kod); } }), ); }).toList(), ), const SizedBox(height: 16), _Baslik( 'Müzik Türleri', 'Karma yapabilirsin (rock + arabesk, mariachi + reggaeton vb.)', ), Wrap( spacing: 6, runSpacing: 6, children: MuzikKatalog.turler.map((t) { final aktif = _turler.contains(t.kod); return FilterChip( label: Text('${t.simge} ${t.etiket}', style: const TextStyle(fontSize: 12)), selected: aktif, onSelected: (v) => setState(() { if (v) { _turler.add(t.kod); } else { _turler.remove(t.kod); } }), ); }).toList(), ), const SizedBox(height: 16), _Baslik('Mood / Atmosfer', 'Birden fazla seçilebilir'), Wrap( spacing: 6, runSpacing: 6, children: MuzikKatalog.moodlar.map((m) { final aktif = _moodlar.contains(m.kod); return FilterChip( label: Text('${m.simge} ${m.etiket}', style: const TextStyle(fontSize: 12)), selected: aktif, onSelected: (v) => setState(() { if (v) { _moodlar.add(m.kod); } else { _moodlar.remove(m.kod); } }), ); }).toList(), ), const SizedBox(height: 16), _Baslik('Tempo: $_bpm BPM', MuzikKatalog.bpmEtiket(_bpm)), Slider( value: _bpm.toDouble(), min: 50, max: 220, divisions: 170, label: '$_bpm BPM', onChanged: (v) => setState(() => _bpm = v.round()), ), const SizedBox(height: 8), _Baslik('Enerji: %$_enerji', MuzikKatalog.enerjiEtiket(_enerji)), Slider( value: _enerji.toDouble(), min: 0, max: 100, divisions: 100, label: '%$_enerji', onChanged: (v) => setState(() => _enerji = v.round()), ), const SizedBox(height: 16), _Baslik('Vokalist', 'Cinsiyet + ses karakteri (çoklu seçim)'), Wrap( spacing: 6, children: MuzikKatalog.cinsiyetSecenek.map((c) { return ChoiceChip( label: Text(c, style: const TextStyle(fontSize: 12)), selected: _cinsiyet == c, onSelected: (_) => setState(() => _cinsiyet = c), ); }).toList(), ), const SizedBox(height: 8), Wrap( spacing: 6, runSpacing: 6, children: MuzikKatalog.vokalKarakterleri.map((v) { final aktif = _vokal.contains(v.kod); return FilterChip( label: Text(v.etiket, style: const TextStyle(fontSize: 12)), selected: aktif, onSelected: (b) => setState(() { if (b) { _vokal.add(v.kod); } else { _vokal.remove(v.kod); } }), ); }).toList(), ), const SizedBox(height: 16), _Baslik('Öne Çıkan Enstrümanlar', 'Karışımda baskın olacak çalgılar'), Wrap( spacing: 6, runSpacing: 6, children: MuzikKatalog.enstrumanlar.map((e) { final aktif = _enstrumanlar.contains(e.kod); return FilterChip( label: Text('${e.simge} ${e.etiket}', style: const TextStyle(fontSize: 12)), selected: aktif, onSelected: (b) => setState(() { if (b) { _enstrumanlar.add(e.kod); } else { _enstrumanlar.remove(e.kod); } }), ); }).toList(), ), const SizedBox(height: 16), _Baslik('Yaratıcılık', 'LM temperature ile eşlenir (düşük=tutarlı, yüksek=yaratıcı)'), Slider( value: _yaraticilik, min: 0, max: 1.5, divisions: 30, label: _yaraticilik.toStringAsFixed(2), onChanged: (v) => setState(() => _yaraticilik = v), ), const SizedBox(height: 16), _Baslik('Ekstra Açıklama', 'Serbest metin — prompta eklenir'), TextField( controller: _notCtrl, maxLines: 3, decoration: const InputDecoration( isDense: true, hintText: 'Ör: gitar ön planda, çift vokal, yağmur efekti', border: OutlineInputBorder(), ), onChanged: (_) => setState(() {}), ), ], ), ), ), const VerticalDivider(width: 1), // Sağ: canlı önizleme Expanded( flex: 2, child: Container( color: theme.colorScheme.surfaceContainerLow, child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.preview, size: 16, color: theme.colorScheme.primary), const SizedBox(width: 6), Text('Önizleme — Otomatik Oluşturulan', style: theme.textTheme.labelLarge), ], ), const SizedBox(height: 12), _OnizlemeKart( etiket: 'PROMPT', icon: Icons.text_fields, metin: sonuc.prompt, vurguRenk: theme.colorScheme.primary, ), const SizedBox(height: 8), _OnizlemeKart( etiket: 'NEGATİF PROMPT', icon: Icons.block, metin: sonuc.negatifPrompt, vurguRenk: Colors.deepOrange, ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 6, children: [ _Etiket('BPM', '${sonuc.bpm}'), _Etiket('Dil', sonuc.dil.toUpperCase()), _Etiket( 'CFG', sonuc.cfg.toStringAsFixed(2)), _Etiket('LM Temp', sonuc.lmTemperature.toStringAsFixed(2)), _Etiket('Vokal', sonuc.cinsiyet), ], ), if (sonuc.onerilenLyricsTaslak.isNotEmpty) ...[ const SizedBox(height: 8), _OnizlemeKart( etiket: 'LYRICS TASLAĞI', icon: Icons.lyrics, metin: sonuc.onerilenLyricsTaslak, vurguRenk: Colors.purple, ozelMonospace: true, ), ], ], ), ), ), ), ], ), ), const Divider(height: 1), // Footer Padding( padding: const EdgeInsets.all(12), child: Row( children: [ TextButton.icon( icon: const Icon(Icons.refresh, size: 16), label: const Text('Sıfırla'), onPressed: () => setState(() { _kulturler.clear(); _turler.clear(); _moodlar.clear(); _vokal.clear(); _enstrumanlar.clear(); _bpm = 110; _enerji = 60; _cinsiyet = 'erkek'; _yaraticilik = 0.85; _notCtrl.clear(); }), ), const Spacer(), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('İptal'), ), const SizedBox(width: 8), FilledButton.icon( icon: const Icon(Icons.check, size: 16), label: const Text('Forma uygula'), onPressed: () { Navigator.of(context).pop(_sonucHesapla()); }, ), ], ), ), ], ), ), ); } } class _Baslik extends StatelessWidget { final String metin; final String? altMetin; const _Baslik(this.metin, [this.altMetin]); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.only(bottom: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(metin, style: theme.textTheme.titleSmall?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, )), if (altMetin != null) Text(altMetin!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.outline, )), ], ), ); } } class _OnizlemeKart extends StatelessWidget { final String etiket; final IconData icon; final String metin; final Color vurguRenk; final bool ozelMonospace; const _OnizlemeKart({ required this.etiket, required this.icon, required this.metin, required this.vurguRenk, this.ozelMonospace = false, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(6), border: Border.all(color: vurguRenk.withValues(alpha: 0.4)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, size: 14, color: vurguRenk), const SizedBox(width: 6), Text(etiket, style: theme.textTheme.labelSmall?.copyWith( color: vurguRenk, fontWeight: FontWeight.bold, )), ], ), const SizedBox(height: 6), SelectableText( metin, style: TextStyle( fontSize: 12, fontFamily: ozelMonospace ? 'monospace' : null, height: 1.4, ), ), ], ), ); } } class _Etiket extends StatelessWidget { final String baslik; final String deger; const _Etiket(this.baslik, this.deger); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text('$baslik: ', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, )), Text(deger, style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.bold, )), ], ), ); } } extension _IterableFirstOrNull on Iterable { E? get firstOrNull { final it = iterator; return it.moveNext() ? it.current : null; } }