import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/contracts/icon_library.dart'; import '../core/infra/icon_library_api/icon_library_client.dart'; import 'shared/settings_persistence.dart'; enum IconLibraryStatus { idle, loading, loaded, error } class IconLibraryEntryState { final String url; final IconLibrary? library; final IconLibraryStatus status; final String? error; const IconLibraryEntryState({ required this.url, this.library, this.status = IconLibraryStatus.idle, this.error, }); IconLibraryEntryState copyWith({ IconLibrary? library, IconLibraryStatus? status, String? error, bool clearError = false, }) => IconLibraryEntryState( url: url, library: library ?? this.library, status: status ?? this.status, error: clearError ? null : (error ?? this.error), ); int get iconCount => library?.icons.length ?? 0; } class IconLibrarySettingsState { final List entries; const IconLibrarySettingsState({this.entries = const []}); IconLibrarySettingsState copyWith({List? entries}) => IconLibrarySettingsState(entries: entries ?? this.entries); int get totalIcons => entries.fold(0, (acc, e) => acc + e.iconCount); } class IconLibrarySettingsNotifier extends AsyncNotifier with SettingsPersistence { static const _keyUrls = 'smplayer.icon_library_urls'; static const _keyCache = 'smplayer.icon_library_cache'; static const _keyInitialized = 'smplayer.icon_library_initialized'; static const defaultUrls = [ 'https://emby-icon.vercel.app/TFEL-Emby.json', 'https://raw.githubusercontent.com/xiyuliu509/Player-Icon/refs/heads/master/invisible.json', 'https://raw.githubusercontent.com/ginibond/ginibond/main/Icons/Forward/tubiao.json', ]; late final IconLibraryClient _client; @override Future build() async { _client = IconLibraryClient(); final prefs = await getPrefs(); final initialized = prefs.getBool(_keyInitialized) ?? false; List urls = prefs.getStringList(_keyUrls) ?? const []; if (!initialized && urls.isEmpty) { urls = List.of(defaultUrls); await persistFields({_keyUrls: urls, _keyInitialized: true}); } else if (!initialized) { await persistField(_keyInitialized, true); } final cacheRaw = prefs.getString(_keyCache); final cache = {}; if (cacheRaw != null && cacheRaw.isNotEmpty) { try { final decoded = jsonDecode(cacheRaw); if (decoded is Map) { for (final e in decoded.entries) { final value = e.value; if (value is Map) { cache[e.key] = IconLibrary.parse(e.key, value); } } } } catch (err) { debugPrint('[IconLibrary] failed to decode cache: $err'); } } final entries = urls .map( (u) => IconLibraryEntryState( url: u, library: cache[u], status: cache[u] != null ? IconLibraryStatus.loaded : IconLibraryStatus.idle, ), ) .toList(); final initialState = IconLibrarySettingsState(entries: entries); Future.microtask(() async { for (final e in entries) { if (e.library == null) { await refresh(e.url); } } }); return initialState; } Future addUrl(String url) async { final trimmed = url.trim(); if (trimmed.isEmpty) return; final current = state.value ?? const IconLibrarySettingsState(); if (current.entries.any((e) => e.url == trimmed)) return; final next = [ ...current.entries, IconLibraryEntryState(url: trimmed, status: IconLibraryStatus.loading), ]; state = AsyncValue.data(current.copyWith(entries: next)); await _persistUrls(next); await refresh(trimmed); } Future removeUrl(String url) async { final current = state.value ?? const IconLibrarySettingsState(); final next = current.entries.where((e) => e.url != url).toList(); if (next.length == current.entries.length) return; state = AsyncValue.data(current.copyWith(entries: next)); await _persistUrls(next); await _persistCache(next); } Future refresh(String url) async { final current = state.value; if (current == null) return; final idx = current.entries.indexWhere((e) => e.url == url); if (idx < 0) return; var entries = List.of(current.entries); entries[idx] = entries[idx].copyWith( status: IconLibraryStatus.loading, clearError: true, ); state = AsyncValue.data(current.copyWith(entries: entries)); try { final lib = await _client.fetch(url); final after = state.value; if (after == null) return; final i = after.entries.indexWhere((e) => e.url == url); if (i < 0) return; entries = List.of(after.entries); entries[i] = entries[i].copyWith( library: lib, status: IconLibraryStatus.loaded, clearError: true, ); state = AsyncValue.data(after.copyWith(entries: entries)); await _persistCache(entries); } catch (err) { debugPrint('[IconLibrary] refresh failed for $url: $err'); final after = state.value; if (after == null) return; final i = after.entries.indexWhere((e) => e.url == url); if (i < 0) return; entries = List.of(after.entries); entries[i] = entries[i].copyWith( status: IconLibraryStatus.error, error: err.toString(), ); state = AsyncValue.data(after.copyWith(entries: entries)); } } Future refreshAll() async { final current = state.value; if (current == null) return; for (final e in current.entries) { await refresh(e.url); } } Future _persistUrls(List entries) async { await persistField(_keyUrls, entries.map((e) => e.url).toList()); } Future _persistCache(List entries) async { final map = {}; for (final e in entries) { final lib = e.library; if (lib != null) map[e.url] = lib.toJson(); } await persistField(_keyCache, jsonEncode(map)); } } final iconLibraryProvider = AsyncNotifierProvider< IconLibrarySettingsNotifier, IconLibrarySettingsState >(IconLibrarySettingsNotifier.new);