import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/contracts/icon_library.dart'; import '../../features/settings/settings_page.dart'; import '../../providers/icon_library_provider.dart'; import 'adaptive_modal.dart'; import 'app_error_state.dart'; import 'app_loading_ring.dart'; import 'app_skeleton.dart'; import '../utils/user_error_formatter.dart'; Future showIconPickerDialog( BuildContext context, { String currentIconUrl = '', }) { return showAdaptiveModal( context: context, maxWidth: 720, maxHeight: 600, compactHeightFactor: 0.88, builder: (_) => _IconPickerDialog(currentIconUrl: currentIconUrl), ); } class _IconPickerDialog extends ConsumerStatefulWidget { final String currentIconUrl; const _IconPickerDialog({required this.currentIconUrl}); @override ConsumerState<_IconPickerDialog> createState() => _IconPickerDialogState(); } class _IconPickerDialogState extends ConsumerState<_IconPickerDialog> { String _query = ''; final _searchController = TextEditingController(); @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final state = ref.watch(iconLibraryProvider); return Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 12), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Expanded( child: Text( '选择服务器图标', style: theme.textTheme.titleMedium, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (widget.currentIconUrl.isNotEmpty) ...[ const SizedBox(width: 8), TextButton.icon( style: TextButton.styleFrom( minimumSize: const Size(0, 44), padding: const EdgeInsets.symmetric(horizontal: 12), ), onPressed: () => Navigator.of(context).pop(''), icon: const Icon(Icons.do_disturb_on_outlined, size: 18), label: const Text('清除图标'), ), ], IconButton( tooltip: '关闭', icon: const Icon(Icons.close, size: 20), onPressed: () => Navigator.of(context).pop(), ), ], ), const SizedBox(height: 8), TextField( controller: _searchController, decoration: const InputDecoration( prefixIcon: Icon(Icons.search, size: 18), hintText: '按名称搜索图标', isDense: true, ), textInputAction: TextInputAction.search, autocorrect: false, enableSuggestions: false, onChanged: (v) => setState(() => _query = v.trim().toLowerCase()), ), const SizedBox(height: 8), Expanded( child: state.when( data: (data) => _buildBody(theme, data), loading: () => const Center(child: AppLoadingRing()), error: (e, _) => AppErrorState( message: formatUserError(e, fallback: '图标库加载失败'), compact: true, onRetry: () => ref.read(iconLibraryProvider.notifier).refreshAll(), ), ), ), ], ), ); } Widget _buildBody(ThemeData theme, IconLibrarySettingsState data) { if (data.entries.isEmpty) { return _EmptyState( title: '尚未配置图标库', message: '前往「设置 → 图标库」添加 JSON 来源 URL', actionLabel: '前往设置', onAction: () { Navigator.of(context).pop(); showSettingsDialog(context, initialTab: 'icon-library'); }, ); } return FTabs( scrollable: true, expands: true, contentPhysics: const NeverScrollableScrollPhysics(), children: [ for (final entry in data.entries) FTabEntry( label: _libraryLabel(entry), child: _buildLibraryTab(entry), ), ], ); } Widget _libraryLabel(IconLibraryEntryState entry) { final name = entry.library?.name; final label = (name != null && name.isNotEmpty) ? name : Uri.tryParse(entry.url)?.host ?? entry.url; return Row( mainAxisSize: MainAxisSize.min, children: [ Text(label, maxLines: 1, overflow: TextOverflow.ellipsis), if (entry.status == IconLibraryStatus.loading) ...[ const SizedBox(width: 6), const AppLoadingRing(size: 10), ], ], ); } Widget _buildLibraryTab(IconLibraryEntryState entry) { final theme = Theme.of(context); if (entry.status == IconLibraryStatus.loading) { return const Center(child: AppLoadingRing()); } if (entry.status == IconLibraryStatus.error) { return AppErrorState( message: entry.error ?? '', compact: true, onRetry: () => ref.read(iconLibraryProvider.notifier).refresh(entry.url), ); } final lib = entry.library; if (lib == null) { return const Center(child: AppLoadingRing()); } final filtered = _query.isEmpty ? lib.icons : lib.icons .where((i) => i.name.toLowerCase().contains(_query)) .toList(); if (filtered.isEmpty) { return Center( child: Text( _query.isNotEmpty ? '无匹配图标' : '图标库为空', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ); } return GridView.builder( padding: const EdgeInsets.symmetric(vertical: 4), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 76, childAspectRatio: 0.85, crossAxisSpacing: 8, mainAxisSpacing: 8, ), itemCount: filtered.length, itemBuilder: (_, i) { final icon = filtered[i]; return _IconTile( entry: icon, selected: icon.url == widget.currentIconUrl, onTap: () => Navigator.of(context).pop(icon.url), ); }, ); } } class _IconTile extends StatelessWidget { final IconEntry entry; final bool selected; final VoidCallback onTap; const _IconTile({ required this.entry, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final accent = theme.colorScheme.primary; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Tooltip( message: entry.name, preferBelow: false, child: Padding( padding: const EdgeInsets.all(4), child: Column( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: Container( decoration: BoxDecoration( border: Border.all( color: selected ? accent : theme.colorScheme.outlineVariant.withValues( alpha: 0.3, ), width: selected ? 2 : 1, ), borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.all(6), child: CachedNetworkImage( imageUrl: entry.url, fit: BoxFit.contain, memCacheWidth: 128, placeholder: (_, _) => const SizedBox.expand(child: AppSkeleton(radius: 6)), errorWidget: (_, _, _) => Icon( Icons.broken_image_outlined, size: 18, color: theme.colorScheme.onSurfaceVariant.withValues( alpha: 0.4, ), ), ), ), ), const SizedBox(height: 4), Text( entry.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.labelSmall?.copyWith( fontSize: 10, color: selected ? accent : theme.colorScheme.onSurfaceVariant, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, ), ), ], ), ), ), ); } } class _EmptyState extends StatelessWidget { final String title; final String message; final String actionLabel; final VoidCallback onAction; const _EmptyState({ required this.title, required this.message, required this.actionLabel, required this.onAction, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.image_search_outlined, size: 36, color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.5), ), const SizedBox(height: 12), Text(title, style: theme.textTheme.titleSmall), const SizedBox(height: 6), Text( message, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), OutlinedButton(onPressed: onAction, child: Text(actionLabel)), ], ), ); } }