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
@@ -0,0 +1,283 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../providers/icon_library_provider.dart';
import '../../../shared/constants/breakpoints.dart';
import '../../../shared/theme/app_theme.dart';
import '../../../shared/utils/user_error_formatter.dart';
import '../../../shared/widgets/adaptive_modal.dart';
import '../../../shared/widgets/app_dialog.dart';
import '../../../shared/widgets/app_loading_ring.dart';
import '../widgets/settings_card.dart';
class IconLibrarySettingsTab extends ConsumerWidget {
const IconLibrarySettingsTab({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return const SettingsTabScroll(
children: [
SettingsCard(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: IconLibrarySection(),
),
],
),
],
);
}
}
class IconLibrarySection extends ConsumerWidget {
const IconLibrarySection({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final tokens = context.appTokens;
final state = ref.watch(iconLibraryProvider);
return state.when(
data: (data) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'配置图标库 JSON URL,添加/编辑服务器时可从中选择图标',
style: theme.textTheme.bodySmall?.copyWith(
color: tokens.mutedForeground,
),
),
const SizedBox(height: 12),
if (data.entries.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
'尚未配置图标库',
style: theme.textTheme.bodySmall?.copyWith(
color: tokens.mutedForeground,
),
),
)
else
...data.entries.map((entry) => IconLibraryRow(entry: entry)),
const SizedBox(height: 8),
Row(
children: [
FilledButton.tonalIcon(
onPressed: () => _showAddDialog(context, ref),
icon: const Icon(Icons.add, size: 16),
label: const Text('添加图标库'),
),
const SizedBox(width: 8),
if (data.entries.isNotEmpty)
TextButton.icon(
onPressed: () =>
ref.read(iconLibraryProvider.notifier).refreshAll(),
icon: const Icon(Icons.refresh, size: 16),
label: const Text('刷新全部'),
),
const Spacer(),
Text(
'${data.totalIcons} 个图标',
style: theme.textTheme.bodySmall?.copyWith(
color: tokens.mutedForeground,
),
),
],
),
],
),
loading: () => const SizedBox.shrink(),
error: (e, _) => settingsLoadError(e),
);
}
void _showAddDialog(BuildContext context, WidgetRef ref) {
final controller = TextEditingController();
void submit(BuildContext ctx) {
final url = controller.text.trim();
if (url.isEmpty) return;
ref.read(iconLibraryProvider.notifier).addUrl(url);
Navigator.pop(ctx);
}
showAdaptiveModal<void>(
context: context,
maxWidth: 420,
compactMaxHeightFactor: 0.9,
builder: (ctx) {
final theme = Theme.of(ctx);
final isCompact = Breakpoints.isCompact(MediaQuery.sizeOf(ctx).width);
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(24, isCompact ? 16 : 24, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('添加图标库', style: theme.textTheme.titleLarge),
const SizedBox(height: 8),
Text(
'输入图标库 JSON URL',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
TextField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
autocorrect: false,
enableSuggestions: false,
onSubmitted: (_) => submit(ctx),
decoration: const InputDecoration(
hintText: 'https://example.com/icons.json',
isDense: true,
),
),
const SizedBox(height: 20),
if (isCompact) ...[
SizedBox(
height: 48,
child: FilledButton(
onPressed: () => submit(ctx),
child: const Text('添加'),
),
),
const SizedBox(height: 8),
SizedBox(
height: 44,
child: OutlinedButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消'),
),
),
] else
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () => submit(ctx),
child: const Text('添加'),
),
],
),
],
),
);
},
).whenComplete(controller.dispose);
}
}
class IconLibraryRow extends ConsumerWidget {
final IconLibraryEntryState entry;
const IconLibraryRow({super.key, required this.entry});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final tokens = context.appTokens;
final notifier = ref.read(iconLibraryProvider.notifier);
final lib = entry.library;
final title = lib?.name.isNotEmpty == true ? lib!.name : entry.url;
final (statusText, statusColor) = switch (entry.status) {
IconLibraryStatus.loading => ('加载中…', tokens.mutedForeground),
IconLibraryStatus.loaded => (
'${entry.iconCount} 个图标',
tokens.mutedForeground,
),
IconLibraryStatus.error => (
'加载失败:${formatUserError(entry.error, fallback: "未知错误")}',
theme.colorScheme.error,
),
IconLibraryStatus.idle => ('未加载', tokens.mutedForeground),
};
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 2),
if (lib?.name.isNotEmpty == true)
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
entry.url,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelSmall?.copyWith(
color: tokens.mutedForeground,
),
),
),
Text(
statusText,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: statusColor,
),
),
],
),
),
if (entry.status == IconLibraryStatus.loading)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
child: AppLoadingRing(size: 14),
)
else
IconButton(
tooltip: '刷新',
icon: const Icon(Icons.refresh, size: 18),
onPressed: () => notifier.refresh(entry.url),
),
IconButton(
tooltip: '移除',
icon: const Icon(Icons.delete_outline, size: 18),
onPressed: () => _confirmRemove(context, notifier, entry.url),
),
],
),
);
}
Future<void> _confirmRemove(
BuildContext context,
IconLibrarySettingsNotifier notifier,
String url,
) async {
final confirmed = await showAppConfirmDialog(
context,
title: '移除图标库',
body: Text('确定移除「$url」?已选用此库图标的服务器将回退为首字母显示。'),
confirmLabel: '移除',
destructive: true,
);
if (confirmed) await notifier.removeUrl(url);
}
}