Initial commit

This commit is contained in:
admin1
2026-07-14 11:11:36 +08:00
commit 656499cf94
604 changed files with 119518 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
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);