import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import '../app/tema.dart'; import '../veri/isar_servisi.dart'; import '../veri/modeller/oturum_taslagi.dart'; import 'civitai_servisi.dart'; const _civitaiTabanlari = [ 'Flux.1 D', 'SDXL 1.0', 'Pony', 'Illustrious', 'SD 1.5', 'SD 3.5', 'Hunyuan Video', 'Wan Video', 'LTXV', ]; /// Civitai API token (gated indirmeler için) — OturumTaslagi'nde saklanır. final civitaiTokenProvider = NotifierProvider(CivitaiTokenN.new); class CivitaiTokenN extends Notifier { @override String build() { final t = isar.oturumTaslagis .filter() .anahtarEqualTo('civitai_token') .findFirstSync(); return t?.durumJson ?? ''; } void ayarla(String token) { state = token; final t = isar.oturumTaslagis .filter() .anahtarEqualTo('civitai_token') .findFirstSync() ?? (OturumTaslagi()..anahtar = 'civitai_token'); t.durumJson = token; isar.writeTxnSync(() => isar.oturumTaslagis.putSync(t)); } } class CivitaiArama extends ConsumerStatefulWidget { const CivitaiArama({super.key}); @override ConsumerState createState() => _CivitaiAramaState(); } class _CivitaiAramaState extends ConsumerState { final _ctrl = TextEditingController(); String _taban = 'Flux.1 D'; bool _nsfw = false; bool _yukleniyor = false; List _sonuclar = []; final _indiriliyor = {}; @override void dispose() { _ctrl.dispose(); super.dispose(); } Future _ara() async { setState(() => _yukleniyor = true); try { final r = await CivitaiServisi.ara( sorgu: _ctrl.text, tabanModel: _taban, nsfw: _nsfw); setState(() => _sonuclar = r); } catch (_) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Civitai aramasında hata'))); } } finally { if (mounted) setState(() => _yukleniyor = false); } } Future _indir(CivitaiLora l) async { setState(() => _indiriliyor[l.versionId] = 0); final ok = await CivitaiServisi.indir( l, apiToken: ref.read(civitaiTokenProvider), onIlerleme: (o) { if (mounted) setState(() => _indiriliyor[l.versionId] = o); }, ); if (mounted) { setState(() => _indiriliyor.remove(l.versionId)); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(ok ? '${l.ad} indirildi — kütüphaneye eklendi' : '${l.ad} indirilemedi (gated ise API token gerekli)'))); } } @override Widget build(BuildContext context) { final token = ref.watch(civitaiTokenProvider); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: SizedBox( height: 36, child: TextField( controller: _ctrl, decoration: const InputDecoration( prefixIcon: Icon(Icons.search, size: 18), hintText: 'Civitai\'de LoRA ara...', isDense: true, ), onSubmitted: (_) => _ara(), ), ), ), const SizedBox(width: 8), FilledButton.icon( onPressed: _yukleniyor ? null : _ara, icon: const Icon(Icons.search, size: 16), label: const Text('Ara'), ), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: SizedBox( height: 30, child: ListView( scrollDirection: Axis.horizontal, children: [ for (final t in _civitaiTabanlari) ...[ ChoiceChip( label: Text(t, style: const TextStyle(fontSize: 11)), selected: _taban == t, onSelected: (_) => setState(() => _taban = t), ), const SizedBox(width: 5), ], ], ), ), ), const SizedBox(width: 8), FilterChip( label: const Text('18+', style: TextStyle(fontSize: 11)), selected: _nsfw, selectedColor: const Color(0xFFFF2E6A).withValues(alpha: 0.25), onSelected: (v) => setState(() => _nsfw = v), ), ], ), const SizedBox(height: 6), // API token (gated indirmeler için) SizedBox( height: 32, child: TextField( decoration: InputDecoration( isDense: true, prefixIcon: const Icon(Icons.key, size: 15), hintText: 'Civitai API Token (opsiyonel, gated LoRA için)', suffixIcon: token.isNotEmpty ? const Icon(Icons.check, size: 15, color: FikoRenkler.neonYesil) : null, ), controller: TextEditingController(text: token), onSubmitted: (v) => ref.read(civitaiTokenProvider.notifier).ayarla(v), ), ), const SizedBox(height: 10), Expanded( child: _yukleniyor ? const Center(child: CircularProgressIndicator()) : _sonuclar.isEmpty ? Center( child: Text('Ara butonuna bas', style: TextStyle( color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.5)))) : GridView.builder( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 240, mainAxisExtent: 250, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: _sonuclar.length, itemBuilder: (_, i) { final l = _sonuclar[i]; final ind = _indiriliyor[l.versionId]; return _SonucKart( lora: l, ilerleme: ind, onIndir: () => _indir(l)); }, ), ), ], ); } } class _SonucKart extends StatelessWidget { const _SonucKart( {required this.lora, required this.ilerleme, required this.onIndir}); final CivitaiLora lora; final double? ilerleme; final VoidCallback onIndir; @override Widget build(BuildContext context) { return Card( clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: Stack( fit: StackFit.expand, children: [ if (lora.onizleme.isNotEmpty) Image.network(lora.onizleme, fit: BoxFit.cover, errorBuilder: (_, _, _) => const ColoredBox(color: Colors.black26)) else const ColoredBox(color: Colors.black26), if (lora.nsfw) Positioned( top: 4, left: 4, child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: const Color(0xFFFF2E6A), borderRadius: BorderRadius.circular(4)), child: const Text('18+', style: TextStyle( fontSize: 9, fontWeight: FontWeight.w800, color: Colors.white)), ), ), Positioned( bottom: 4, right: 4, child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(4)), child: Text('${lora.tabanModel} • ${lora.boyutMb.round()}MB', style: const TextStyle( fontSize: 8.5, color: Colors.white)), ), ), ], ), ), Padding( padding: const EdgeInsets.all(6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(lora.ad, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 11.5, fontWeight: FontWeight.w700)), const SizedBox(height: 4), SizedBox( height: 26, width: double.infinity, child: ilerleme != null ? Row(children: [ Expanded( child: LinearProgressIndicator( value: ilerleme == 0 ? null : ilerleme)), const SizedBox(width: 6), Text('%${(ilerleme! * 100).round()}', style: const TextStyle(fontSize: 10)), ]) : FilledButton.icon( style: FilledButton.styleFrom( visualDensity: VisualDensity.compact, padding: EdgeInsets.zero), onPressed: onIndir, icon: const Icon(Icons.download, size: 15), label: const Text('İndir', style: TextStyle(fontSize: 11)), ), ), ], ), ), ], ), ); } }