import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' as p; import '../app/tema.dart'; import '../cekirdek/sabitler.dart'; import 'model_registry.dart'; import 'model_servisi.dart'; class ModelEkrani extends ConsumerStatefulWidget { const ModelEkrani({super.key}); @override ConsumerState createState() => _ModelEkraniState(); } class _ModelEkraniState extends ConsumerState { // url → indirme oranı (0-1) final Map _indiriliyor = {}; final Map _iptaller = {}; Map> _kuruluHarita = {}; String _yerelYol(ModelKaydi m) => p.join(Sabitler.comfyModelsKlasoru, m.klasor, m.dosyaAdi); bool _kurulu(ModelKaydi m) { // 1) ComfyUI'nin gerçek listesinde var mı (basename eşleşmesi) final set = _kuruluHarita[m.klasor]; if (set != null && set.contains(m.dosyaAdi.toLowerCase())) return true; // 2) Dosya yolu fallback return File(_yerelYol(m)).existsSync(); } Future _indir(ModelKaydi m) async { final hedef = _yerelYol(m); await Directory(p.dirname(hedef)).create(recursive: true); final iptal = CancelToken(); setState(() { _indiriliyor[m.url] = 0; _iptaller[m.url] = iptal; }); try { final dio = Dio(); await dio.download( m.url, hedef, cancelToken: iptal, options: Options(followRedirects: true, maxRedirects: 6), onReceiveProgress: (a, b) { if (b > 0 && mounted) setState(() => _indiriliyor[m.url] = a / b); }, ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${m.ad} indirildi ✓'))); } } catch (e) { // İptal değilse hata göster, yarım dosyayı sil if (iptal.isCancelled) { File(hedef).existsSync() ? File(hedef).deleteSync() : null; } else if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${m.ad} indirilemedi'))); } } finally { if (mounted) { setState(() { _indiriliyor.remove(m.url); _iptaller.remove(m.url); }); } } } @override Widget build(BuildContext context) { final gruplu = ModelRegistry.gruplu; final kuruluAsync = ref.watch(kuruluModellerProvider); _kuruluHarita = kuruluAsync.value ?? {}; final toplamKurulu = _kuruluHarita.values.fold(0, (a, s) => a + s.length); return Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.download_for_offline, color: FikoRenkler.neonCyan, size: 20), const SizedBox(width: 8), const Text('Model Yöneticisi', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w800)), const SizedBox(width: 12), if (kuruluAsync.isLoading) const SizedBox( width: 13, height: 13, child: CircularProgressIndicator(strokeWidth: 2)) else Text('ComfyUI\'de kurulu: $toplamKurulu model', style: const TextStyle( fontSize: 12, color: FikoRenkler.neonYesil)), const Spacer(), IconButton( tooltip: 'Kurulu listesini yenile', icon: const Icon(Icons.refresh, size: 18), onPressed: () => ref.invalidate(kuruluModellerProvider), ), ], ), const SizedBox(height: 10), Expanded( child: ListView( children: [ for (final giris in gruplu.entries) ...[ Padding( padding: const EdgeInsets.only(top: 6, bottom: 6), child: Row(children: [ Container( width: 4, height: 16, decoration: BoxDecoration( color: FikoRenkler.neonMor, borderRadius: BorderRadius.circular(2)), ), const SizedBox(width: 8), Text(giris.key, style: const TextStyle( fontSize: 13.5, fontWeight: FontWeight.w800)), ]), ), for (final m in giris.value) _modelSatir(m), ], ], ), ), ], ), ); } Widget _modelSatir(ModelKaydi m) { final kurulu = _kurulu(m); final oran = _indiriliyor[m.url]; return Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(9), border: Border.all( color: kurulu ? FikoRenkler.neonYesil.withValues(alpha: 0.4) : Theme.of(context).dividerColor), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Flexible( child: Text(m.ad, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12.5, fontWeight: FontWeight.w600)), ), const SizedBox(width: 6), if (m.zorunlu) const _Mini('zorunlu', FikoRenkler.neonAmber), if (m.boyutGb > 0) _Mini('${m.boyutGb} GB', FikoRenkler.neonCyan), ]), if (m.aciklama.isNotEmpty) Text(m.aciklama, style: TextStyle( fontSize: 10, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.5))), Text('${m.klasor}/${m.dosyaAdi}', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 9, color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.35))), ], ), ), const SizedBox(width: 10), SizedBox( width: 150, child: oran != null ? Row(children: [ Expanded( child: LinearProgressIndicator( value: oran == 0 ? null : oran)), const SizedBox(width: 6), Text('%${(oran * 100).round()}', style: const TextStyle(fontSize: 10)), IconButton( visualDensity: VisualDensity.compact, icon: const Icon(Icons.close, size: 15), onPressed: () => _iptaller[m.url]?.cancel(), ), ]) : kurulu ? Row( mainAxisAlignment: MainAxisAlignment.end, children: const [ Icon(Icons.check_circle, size: 16, color: FikoRenkler.neonYesil), SizedBox(width: 4), Text('Kurulu', style: TextStyle( fontSize: 11, color: FikoRenkler.neonYesil)), ], ) : FilledButton.icon( style: FilledButton.styleFrom( visualDensity: VisualDensity.compact), icon: const Icon(Icons.download, size: 15), label: const Text('İndir', style: TextStyle(fontSize: 11.5)), onPressed: () => _indir(m), ), ), ], ), ); } } class _Mini extends StatelessWidget { const _Mini(this.metin, this.renk); final String metin; final Color renk; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(right: 4), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: renk.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4)), child: Text(metin, style: TextStyle(fontSize: 9, color: renk)), ); } }