225 lines
6.5 KiB
Dart
225 lines
6.5 KiB
Dart
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<IconLibraryEntryState> entries;
|
|
|
|
const IconLibrarySettingsState({this.entries = const []});
|
|
|
|
IconLibrarySettingsState copyWith({List<IconLibraryEntryState>? entries}) =>
|
|
IconLibrarySettingsState(entries: entries ?? this.entries);
|
|
|
|
int get totalIcons => entries.fold<int>(0, (acc, e) => acc + e.iconCount);
|
|
}
|
|
|
|
class IconLibrarySettingsNotifier
|
|
extends AsyncNotifier<IconLibrarySettingsState>
|
|
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 = <String>[
|
|
'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<IconLibrarySettingsState> build() async {
|
|
_client = IconLibraryClient();
|
|
|
|
final prefs = await getPrefs();
|
|
final initialized = prefs.getBool(_keyInitialized) ?? false;
|
|
List<String> 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 = <String, IconLibrary>{};
|
|
if (cacheRaw != null && cacheRaw.isNotEmpty) {
|
|
try {
|
|
final decoded = jsonDecode(cacheRaw);
|
|
if (decoded is Map<String, dynamic>) {
|
|
for (final e in decoded.entries) {
|
|
final value = e.value;
|
|
if (value is Map<String, dynamic>) {
|
|
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<void> 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<void> 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<void> 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<IconLibraryEntryState>.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<IconLibraryEntryState>.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<IconLibraryEntryState>.of(after.entries);
|
|
entries[i] = entries[i].copyWith(
|
|
status: IconLibraryStatus.error,
|
|
error: err.toString(),
|
|
);
|
|
state = AsyncValue.data(after.copyWith(entries: entries));
|
|
}
|
|
}
|
|
|
|
|
|
Future<void> refreshAll() async {
|
|
final current = state.value;
|
|
if (current == null) return;
|
|
for (final e in current.entries) {
|
|
await refresh(e.url);
|
|
}
|
|
}
|
|
|
|
Future<void> _persistUrls(List<IconLibraryEntryState> entries) async {
|
|
await persistField(_keyUrls, entries.map((e) => e.url).toList());
|
|
}
|
|
|
|
Future<void> _persistCache(List<IconLibraryEntryState> entries) async {
|
|
final map = <String, dynamic>{};
|
|
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);
|