import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../durum/karakter_durumu.dart'; import '../../durum/lora_durumu.dart'; /// Form'da gösterilecek LoRA seçici — modele göre filtreli. class LoraSeciciPaneli extends ConsumerWidget { final String aktifModelRepo; // form'da seçili model repo const LoraSeciciPaneli({super.key, required this.aktifModelRepo}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final tumLoralar = ref.watch(loraListesiProvider); final aktifSecimler = ref.watch(aktifLoralarProvider); final isler = ref.watch(loraIsleriProvider); // Uyumlu LoRA'ları filtrele (base_model aynı veya boş ise) bool uyumlu(Lora l) { if (aktifModelRepo.isEmpty) return true; if (l.baseModel.isEmpty) return true; // Aynı SDXL ailesi → uyumlu varsayılır return l.baseModel == aktifModelRepo; } final aktifIsler = isler.values.where((i) => i.durum == 0).toList(); return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.amber.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.amber.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.school_outlined, size: 14, color: Colors.amber), const SizedBox(width: 6), Text('LoRA\'lar (Karakter / Stil)', style: theme.textTheme.labelMedium?.copyWith( color: Colors.amber.shade800, fontWeight: FontWeight.w600, )), const Spacer(), if (aktifIsler.isNotEmpty) ...[ const Icon(Icons.fiber_manual_record, size: 8, color: Colors.green), const SizedBox(width: 4), Text('${aktifIsler.length} eğitim', style: theme.textTheme.labelSmall?.copyWith( color: Colors.green, fontSize: 10, )), const SizedBox(width: 8), ], IconButton( icon: const Icon(Icons.refresh, size: 14), tooltip: 'Listeyi yenile', visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () => ref.read(loraListesiProvider.notifier).yenile(), ), ], ), const SizedBox(height: 4), // Aktif eğitim işleri for (final i in aktifIsler) _EgitimSatiri(isi: i, onIptal: () => ref.read(loraIsleriProvider.notifier).iptal(i.id)), if (aktifIsler.isNotEmpty) const Divider(height: 16), if (tumLoralar.isEmpty) Padding( padding: const EdgeInsets.all(8), child: Text( 'Henüz LoRA yok. Karakter detayında "LoRA Eğit" ile yeni ' 'karakter LoRA\'sı eğit.', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, ), ), ) else Wrap( spacing: 6, runSpacing: 6, children: tumLoralar.map((l) { final secili = aktifSecimler.indexWhere((a) => a.yol == l.yol); final secilimi = secili >= 0; final uyumluMu = uyumlu(l); return _LoraChip( lora: l, secili: secilimi, uyumlu: uyumluMu, olcek: secilimi ? aktifSecimler[secili].olcek : 0.8, onSec: () { final liste = List.from(aktifSecimler); if (secilimi) { liste.removeAt(secili); } else { liste.add(AktifLoraSecimi( yol: l.yol, olcek: 0.8, tetikleyici: l.tetikleyici, )); } ref.read(aktifLoralarProvider.notifier).state = liste; }, onOlcekDegisti: (v) { final liste = List.from(aktifSecimler); if (secili >= 0) { liste[secili].olcek = v; ref.read(aktifLoralarProvider.notifier).state = liste; } }, onSil: () async { final ok = await showDialog( context: context, builder: (c) => AlertDialog( title: Text('${l.id} silinsin mi?'), content: const Text( 'LoRA dosyası diskten kaldırılır.'), actions: [ TextButton( onPressed: () => Navigator.pop(c, false), child: const Text('İptal')), FilledButton( onPressed: () => Navigator.pop(c, true), child: const Text('Sil')), ], ), ); if (ok == true) { await ref .read(loraListesiProvider.notifier) .sil(l.id); } }, ); }).toList(), ), if (aktifSecimler.isNotEmpty) ...[ const SizedBox(height: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 4), decoration: BoxDecoration( color: Colors.green.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Text( '${aktifSecimler.length} LoRA aktif. ' 'Tetikleyiciler prompt\'a otomatik eklenmez — ' 'gerekirse promptun başına ekle: ' '${aktifSecimler.map((e) => e.tetikleyici ?? "").where((t) => t.isNotEmpty).join(", ")}', style: theme.textTheme.labelSmall?.copyWith( fontSize: 10, color: Colors.green.shade900, ), ), ), ], ], ), ); } } class _LoraChip extends StatelessWidget { final Lora lora; final bool secili; final bool uyumlu; final double olcek; final VoidCallback onSec; final void Function(double) onOlcekDegisti; final VoidCallback onSil; const _LoraChip({ required this.lora, required this.secili, required this.uyumlu, required this.olcek, required this.onSec, required this.onOlcekDegisti, required this.onSil, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final goster = lora.isim ?? lora.id; return Container( decoration: BoxDecoration( color: secili ? Colors.amber.withValues(alpha: 0.18) : theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), border: Border.all( color: secili ? Colors.amber : (uyumlu ? theme.dividerColor : Colors.red.withValues(alpha: 0.4)), width: secili ? 1.5 : 1, ), ), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Checkbox( value: secili, onChanged: uyumlu ? (_) => onSec() : null, visualDensity: VisualDensity.compact, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), const SizedBox(width: 4), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 140), child: Text( goster, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: uyumlu ? null : theme.colorScheme.outline, ), ), ), const SizedBox(width: 4), Tooltip( message: 'Base: ${lora.baseModel}\n' 'Tetikleyici: ${lora.tetikleyici}\n' 'Adım: ${lora.adim}\n' 'Boyut: ${lora.boyutMb} MB', child: Icon(Icons.info_outline, size: 11, color: theme.colorScheme.outline), ), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.delete_outline, size: 12, color: Colors.red), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: onSil, ), ], ), if (!uyumlu) Padding( padding: const EdgeInsets.only(left: 32, top: 2), child: Text( '⚠ ${lora.baseModel.split("/").last} ile eğitildi', style: TextStyle( fontSize: 9, color: Colors.red.shade700, ), ), ), if (secili) Padding( padding: const EdgeInsets.only(left: 32, right: 4, top: 2), child: Row( children: [ Text('Ölçek', style: theme.textTheme.labelSmall ?.copyWith(fontSize: 10)), Expanded( child: SliderTheme( data: SliderThemeData( trackHeight: 2, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 6, ), ), child: Slider( value: olcek.clamp(0.0, 1.5), min: 0.0, max: 1.5, divisions: 30, onChanged: onOlcekDegisti, ), ), ), Text( olcek.toStringAsFixed(2), style: const TextStyle( fontSize: 10, fontFamily: 'monospace', fontWeight: FontWeight.w600, ), ), ], ), ), ], ), ); } } class _EgitimSatiri extends StatelessWidget { final LoraEgitimIsi isi; final VoidCallback onIptal; const _EgitimSatiri({required this.isi, required this.onIptal}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const SizedBox( width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2), ), const SizedBox(width: 6), Expanded( child: Text( '${isi.loraIsim} — ${isi.faz}', style: theme.textTheme.labelSmall?.copyWith( fontWeight: FontWeight.w600, ), ), ), Text('%${isi.ilerleme}', style: theme.textTheme.labelSmall?.copyWith( fontFamily: 'monospace', )), IconButton( icon: const Icon(Icons.cancel_outlined, size: 14, color: Colors.red), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: onIptal, ), ], ), ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( value: isi.ilerleme / 100, minHeight: 3, ), ), ], ), ); } } // ===================================================================== // Eğitim başlatma dialog'u (karakter detayında "LoRA Eğit" butonu) // ===================================================================== class LoraEgitDialog extends ConsumerStatefulWidget { final Karakter karakter; final String veriKlasoruYolu; // karakter yüz seti yolu final List<({String repo, String etiket})> modelSecenekleri; /// Doluysa: mevcut LoRA üzerine "devam et" modu açılır. final Lora? mevcutLora; const LoraEgitDialog({ super.key, required this.karakter, required this.veriKlasoruYolu, required this.modelSecenekleri, this.mevcutLora, }); static Future goster( BuildContext context, { required Karakter karakter, required String veriKlasoruYolu, required List<({String repo, String etiket})> modelSecenekleri, Lora? mevcutLora, }) async { await showDialog( context: context, builder: (_) => LoraEgitDialog( karakter: karakter, veriKlasoruYolu: veriKlasoruYolu, modelSecenekleri: modelSecenekleri, mevcutLora: mevcutLora, ), ); } @override ConsumerState createState() => _LoraEgitDialogState(); } class _LoraEgitDialogState extends ConsumerState { late TextEditingController _isimCtrl; late TextEditingController _tetikleyiciCtrl; String? _baseRepo; int _adim = 1500; int _rank = 16; int _boyut = 1024; String _cins = 'person'; /// 'sifirdan' | 'devam' | 'devam_yeni' String _mod = 'sifirdan'; // Pro features bool _useDora = false; double _minSnrGamma = 0.0; // 0 = kapalı, 5.0 = standart bool _smartCaption = false; bool _autoTest = true; // varsayılan açık (sadece 3 dk ekstra) bool _proMod = false; int _veriSayisi = 0; @override void initState() { super.initState(); final mevcut = widget.mevcutLora; if (mevcut != null) { _mod = 'devam'; _isimCtrl = TextEditingController(text: mevcut.isim ?? mevcut.id); _tetikleyiciCtrl = TextEditingController(text: mevcut.tetikleyici); _baseRepo = mevcut.baseModel.isNotEmpty ? mevcut.baseModel : (widget.modelSecenekleri.isNotEmpty ? widget.modelSecenekleri.first.repo : null); _cins = mevcut.cins; _rank = (mevcut.meta['rank'] as num?)?.toInt() ?? 16; _boyut = (mevcut.meta['boyut'] as num?)?.toInt() ?? 1024; _adim = 500; // ek eğitim için varsayılan } else { _isimCtrl = TextEditingController(text: '${widget.karakter.isim}_v1'); final tetik = '${widget.karakter.isim.replaceAll(RegExp(r"[^A-Za-z0-9]"), "").toLowerCase()}_xyz'; _tetikleyiciCtrl = TextEditingController(text: tetik); _baseRepo = widget.modelSecenekleri.isNotEmpty ? widget.modelSecenekleri.first.repo : null; _cins = widget.karakter.cinsiyet == 'kadın' ? 'woman' : (widget.karakter.cinsiyet == 'erkek' ? 'man' : 'person'); } _veriSayisi = _veriDosyaSayisi(); } int _veriDosyaSayisi() { final d = Directory(widget.veriKlasoruYolu); if (!d.existsSync()) return 0; return d .listSync() .whereType() .where((f) { final ext = f.path.toLowerCase(); return ext.endsWith('.png') || ext.endsWith('.jpg') || ext.endsWith('.jpeg') || ext.endsWith('.webp'); }) .length; } @override void dispose() { _isimCtrl.dispose(); _tetikleyiciCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final yeterliVeri = _veriSayisi >= 5; final mevcut = widget.mevcutLora; return AlertDialog( title: Row( children: [ Icon( mevcut != null ? Icons.update : Icons.school_outlined, color: Colors.amber, ), const SizedBox(width: 8), Expanded( child: Text(mevcut != null ? 'LoRA Geliştir — ${mevcut.isim ?? mevcut.id}' : 'LoRA Eğit — ${widget.karakter.isim}'), ), ], ), content: SizedBox( width: 540, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // ===== Mod seçici (sadece mevcut LoRA varsa) ===== if (mevcut != null) ...[ Text('Eğitim Modu', style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w600, )), const SizedBox(height: 6), SegmentedButton( segments: const [ ButtonSegment( value: 'devam', label: Text('Aynı dosyaya devam', style: TextStyle(fontSize: 11)), icon: Icon(Icons.update, size: 14), ), ButtonSegment( value: 'devam_yeni', label: Text('Devam → yeni dosya', style: TextStyle(fontSize: 11)), icon: Icon(Icons.fork_right, size: 14), ), ButtonSegment( value: 'sifirdan', label: Text('Sıfırdan', style: TextStyle(fontSize: 11)), icon: Icon(Icons.refresh, size: 14), ), ], selected: {_mod}, onSelectionChanged: (s) => setState(() => _mod = s.first), ), const SizedBox(height: 6), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest .withValues(alpha: 0.5), borderRadius: BorderRadius.circular(4), ), child: Text( _mod == 'devam' ? 'Mevcut weights üzerine ek adım eğitilir, ' 'sonuç AYNI dosyaya yazılır (LoRA güncellenir).' : _mod == 'devam_yeni' ? 'Mevcut weights\'ten devam eder ama YENİ dosya ' 'oluşturur (eski LoRA kalır).' : 'Sıfırdan yeni LoRA eğitilir, mevcut weights ' 'yüklenmez.', style: theme.textTheme.labelSmall, ), ), const SizedBox(height: 12), ], // Veri durumu Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: yeterliVeri ? Colors.green.withValues(alpha: 0.08) : Colors.red.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(6), border: Border.all( color: yeterliVeri ? Colors.green.withValues(alpha: 0.4) : Colors.red.withValues(alpha: 0.4), ), ), child: Row( children: [ Icon( yeterliVeri ? Icons.check_circle : Icons.error_outline, color: yeterliVeri ? Colors.green : Colors.red, size: 16, ), const SizedBox(width: 8), Expanded( child: Text( yeterliVeri ? '$_veriSayisi yüz fotoğrafı bulundu — eğitime hazır' : 'Sadece $_veriSayisi fotoğraf — en az 5 (önerilen 10-20) gerekli. ' 'Karakter düzenleme dialogundaki "Yüz Seti" ile foto ekle.', style: theme.textTheme.bodySmall, ), ), ], ), ), const SizedBox(height: 16), // İsim TextField( controller: _isimCtrl, decoration: const InputDecoration( labelText: 'LoRA İsmi (dosya adı)', border: OutlineInputBorder(), isDense: true, helperText: 'Örn. burcu_v1', ), ), const SizedBox(height: 12), // Tetikleyici TextField( controller: _tetikleyiciCtrl, decoration: const InputDecoration( labelText: 'Tetikleyici Kelime (model bunu öğrenir)', border: OutlineInputBorder(), isDense: true, helperText: 'Eşsiz, anlamsız bir kelime — prompt\'ta bu kelime LoRA\'yı tetikler', ), ), const SizedBox(height: 12), // Base model DropdownButtonFormField( value: _baseRepo, decoration: const InputDecoration( labelText: 'Base Model (LoRA bu modele bağlı eğitilir)', border: OutlineInputBorder(), isDense: true, ), items: widget.modelSecenekleri .map((m) => DropdownMenuItem( value: m.repo, child: Text(m.etiket, overflow: TextOverflow.ellipsis), )) .toList(), onChanged: (v) => setState(() => _baseRepo = v), ), const SizedBox(height: 12), DropdownButtonFormField( value: _cins, decoration: const InputDecoration( labelText: 'Sınıf (gen prompt için)', border: OutlineInputBorder(), isDense: true, ), items: const [ DropdownMenuItem(value: 'woman', child: Text('woman')), DropdownMenuItem(value: 'man', child: Text('man')), DropdownMenuItem(value: 'person', child: Text('person')), ], onChanged: (v) => setState(() => _cins = v ?? 'person'), ), const SizedBox(height: 16), Text( mevcut != null && _mod != 'sifirdan' ? 'Ek Adım Sayısı: $_adim' : 'Adım Sayısı: $_adim', style: theme.textTheme.labelMedium), Slider( value: _adim.toDouble(), min: mevcut != null && _mod != 'sifirdan' ? 100 : 500, max: 5000, divisions: 49, label: _adim.toString(), onChanged: (v) => setState(() => _adim = v.round()), ), Text( mevcut != null && _mod != 'sifirdan' ? '100-500: ince ayar; 500-1000: belirgin değişim. ' 'Toplam (eski + ek): ~${(mevcut.adim) + _adim} adım.' : '500-1000: hızlı; 1500-2500: dengeli (önerilir); 3000+: aşırı ' 'öğrenme riski. RTX 4070\'te ~2 dk/100 adım.', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, fontSize: 10, ), ), const SizedBox(height: 12), Text('Rank (LoRA dosya boyutu): $_rank', style: theme.textTheme.labelMedium), Slider( value: _rank.toDouble(), min: 4, max: 64, divisions: 15, label: _rank.toString(), onChanged: (v) => setState(() => _rank = v.round()), ), Text( '8-16 karakter için yeterli. 32-64 daha detaylı stil için.', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, fontSize: 10, ), ), const SizedBox(height: 12), Text('Eğitim Çözünürlüğü: $_boyut', style: theme.textTheme.labelMedium), SegmentedButton( segments: const [ ButtonSegment(value: 512, label: Text('512')), ButtonSegment(value: 768, label: Text('768')), ButtonSegment(value: 1024, label: Text('1024')), ], selected: {_boyut}, onSelectionChanged: (s) => setState(() => _boyut = s.first), ), // ========== PRO FEATURES ========== const SizedBox(height: 16), InkWell( onTap: () => setState(() => _proMod = !_proMod), child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8), decoration: BoxDecoration( color: _proMod ? Colors.deepPurple.withValues(alpha: 0.1) : theme.colorScheme.surfaceContainerHighest .withValues(alpha: 0.3), borderRadius: BorderRadius.circular(6), border: Border.all( color: _proMod ? Colors.deepPurple.withValues(alpha: 0.4) : theme.dividerColor, ), ), child: Row( children: [ Icon( _proMod ? Icons.expand_less : Icons.expand_more, size: 18, color: _proMod ? Colors.deepPurple : theme.colorScheme.outline, ), const SizedBox(width: 6), Icon(Icons.auto_awesome, size: 14, color: Colors.deepPurple), const SizedBox(width: 4), Text( 'Pro Özellikler ${_proMod ? "" : "(tıkla aç)"}', style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w600, color: _proMod ? Colors.deepPurple : theme.colorScheme.outline, ), ), const Spacer(), if (_useDora || _smartCaption || _minSnrGamma > 0 || _autoTest) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.deepPurple, borderRadius: BorderRadius.circular(8), ), child: Text( '${[ if (_useDora) 'DoRA', if (_smartCaption) 'WD14', if (_minSnrGamma > 0) 'SNR', if (_autoTest) 'Test' ].length} aktif', style: const TextStyle( fontSize: 9, color: Colors.white, fontWeight: FontWeight.w600, ), ), ), ], ), ), ), if (_proMod) ...[ const SizedBox(height: 12), _ProToggle( baslik: 'DoRA (Weight-Decomposed)', aciklama: 'Subject fidelity ↑%10-20, eğitim ~%30 yavaş. ' 'Düşük rank için ideal.', ikon: Icons.architecture, aktif: _useDora, onDegisti: (v) => setState(() => _useDora = v), ), const SizedBox(height: 8), _ProToggle( baslik: 'Smart Captioning (WD14)', aciklama: 'Her foto için otomatik tag. Yüz/saç/göz ' 'tag\'leri filtrelenir → LoRA esnekliği ↑.', ikon: Icons.label_outline, aktif: _smartCaption, onDegisti: (v) => setState(() => _smartCaption = v), ), const SizedBox(height: 8), _ProToggle( baslik: 'Min-SNR-gamma Loss', aciklama: 'SNR-ağırlıklı loss → daha hızlı/stabil yakınsama. ' 'Standart: gamma=5.0', ikon: Icons.functions, aktif: _minSnrGamma > 0, onDegisti: (v) => setState(() => _minSnrGamma = v ? 5.0 : 0.0), ), if (_minSnrGamma > 0) ...[ const SizedBox(height: 4), Padding( padding: const EdgeInsets.only(left: 28, right: 8), child: Row( children: [ Text('gamma: ${_minSnrGamma.toStringAsFixed(1)}', style: theme.textTheme.labelSmall), Expanded( child: Slider( value: _minSnrGamma, min: 1.0, max: 10.0, divisions: 18, label: _minSnrGamma.toStringAsFixed(1), onChanged: (v) => setState(() => _minSnrGamma = v), ), ), ], ), ), ], const SizedBox(height: 8), _ProToggle( baslik: 'Otomatik Kalite Testi', aciklama: 'Eğitim sonrası 8 prompt ile gen + ArcFace ' 'similarity. Skor + karar üretir (~3 dk ek).', ikon: Icons.verified_outlined, aktif: _autoTest, onDegisti: (v) => setState(() => _autoTest = v), ), ], ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('İptal'), ), FilledButton.icon( icon: Icon(mevcut != null && _mod != 'sifirdan' ? Icons.update : Icons.play_arrow), label: Text(mevcut != null && _mod != 'sifirdan' ? 'Eğitime Devam Et' : 'Eğitimi Başlat'), onPressed: !yeterliVeri || _baseRepo == null ? null : () async { final devamMod = mevcut != null && _mod != 'sifirdan'; final jobId = await ref .read(loraIsleriProvider.notifier) .egitBaslat( karakterId: widget.karakter.id, loraIsim: _isimCtrl.text.trim(), veriKlasoru: widget.veriKlasoruYolu, baseModelRepo: _baseRepo!, tetikleyici: _tetikleyiciCtrl.text.trim().isEmpty ? null : _tetikleyiciCtrl.text.trim(), cins: _cins, adim: _adim, rank: _rank, mevcutLoraYolu: devamMod ? mevcut.yol : null, ayniDosyayaYaz: _mod == 'devam', useDora: _useDora, minSnrGamma: _minSnrGamma, smartCaption: _smartCaption, autoTest: _autoTest, ); if (context.mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(jobId != null ? (devamMod ? 'Eğitime devam ediliyor (job: $jobId)' : 'Eğitim başladı (job: $jobId)') : 'Eğitim başlatılamadı'), backgroundColor: jobId != null ? Colors.green : Colors.red, ), ); } }, ), ], ); } } /// Pro özellik toggle satırı — ikon + başlık + açıklama + switch class _ProToggle extends StatelessWidget { final String baslik; final String aciklama; final IconData ikon; final bool aktif; final ValueChanged onDegisti; const _ProToggle({ required this.baslik, required this.aciklama, required this.ikon, required this.aktif, required this.onDegisti, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return InkWell( onTap: () => onDegisti(!aktif), borderRadius: BorderRadius.circular(6), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( ikon, size: 16, color: aktif ? Colors.deepPurple : theme.colorScheme.outline, ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( baslik, style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w600, color: aktif ? Colors.deepPurple : null, ), ), Text( aciklama, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, fontSize: 10, ), ), ], ), ), Switch( value: aktif, onChanged: onDegisti, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ], ), ), ); } }