- Create IconPicker widget with searchable grid of Material icons - Create QuickLinkForm with full CRUD support (create, edit, delete) - Update QuickLinkSettingsContent to use real form instead of placeholder - Update QuickLinksPanel to use shared getIconData function - Form includes name, URL, category, icon, type, and active toggle - Validation for required fields and URL format 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
466 lines
12 KiB
Dart
466 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// A widget that displays a grid of Material icons for selection.
|
|
///
|
|
/// Shows a button that opens a dialog with available icons.
|
|
class IconPicker extends StatelessWidget {
|
|
const IconPicker({
|
|
super.key,
|
|
required this.selectedIcon,
|
|
required this.onChanged,
|
|
this.label = 'Icon',
|
|
});
|
|
|
|
/// Currently selected icon name.
|
|
final String selectedIcon;
|
|
|
|
/// Callback when an icon is selected.
|
|
final ValueChanged<String> onChanged;
|
|
|
|
/// Label for the field.
|
|
final String label;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final textTheme = Theme.of(context).textTheme;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: textTheme.labelMedium?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
InkWell(
|
|
onTap: () => _showIconPicker(context),
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: colorScheme.outline),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.primaryContainer,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
getIconData(selectedIcon),
|
|
color: colorScheme.onPrimaryContainer,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
selectedIcon,
|
|
style: textTheme.bodyLarge,
|
|
),
|
|
),
|
|
Icon(
|
|
Icons.arrow_drop_down,
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _showIconPicker(BuildContext context) async {
|
|
final result = await showDialog<String>(
|
|
context: context,
|
|
builder: (context) => IconPickerDialog(
|
|
selectedIcon: selectedIcon,
|
|
onSelected: (icon) => Navigator.of(context).pop(icon),
|
|
),
|
|
);
|
|
|
|
if (result != null) {
|
|
onChanged(result);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Dialog that displays a grid of icons.
|
|
class IconPickerDialog extends StatefulWidget {
|
|
const IconPickerDialog({
|
|
super.key,
|
|
required this.selectedIcon,
|
|
required this.onSelected,
|
|
});
|
|
|
|
final String selectedIcon;
|
|
final ValueChanged<String> onSelected;
|
|
|
|
@override
|
|
State<IconPickerDialog> createState() => _IconPickerDialogState();
|
|
}
|
|
|
|
class _IconPickerDialogState extends State<IconPickerDialog> {
|
|
String _searchQuery = '';
|
|
|
|
List<String> get _filteredIcons {
|
|
if (_searchQuery.isEmpty) {
|
|
return availableIcons;
|
|
}
|
|
return availableIcons
|
|
.where((icon) => icon.toLowerCase().contains(_searchQuery.toLowerCase()))
|
|
.toList();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return Dialog(
|
|
child: Container(
|
|
width: 400,
|
|
height: 500,
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Select Icon',
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
decoration: const InputDecoration(
|
|
hintText: 'Search icons...',
|
|
prefixIcon: Icon(Icons.search),
|
|
border: OutlineInputBorder(),
|
|
isDense: true,
|
|
),
|
|
onChanged: (value) => setState(() => _searchQuery = value),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Expanded(
|
|
child: GridView.builder(
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 5,
|
|
mainAxisSpacing: 8,
|
|
crossAxisSpacing: 8,
|
|
),
|
|
itemCount: _filteredIcons.length,
|
|
itemBuilder: (context, index) {
|
|
final iconName = _filteredIcons[index];
|
|
final isSelected = iconName == widget.selectedIcon;
|
|
|
|
return InkWell(
|
|
onTap: () => widget.onSelected(iconName),
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? colorScheme.primaryContainer
|
|
: colorScheme.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: isSelected
|
|
? Border.all(color: colorScheme.primary, width: 2)
|
|
: null,
|
|
),
|
|
child: Tooltip(
|
|
message: iconName,
|
|
child: Icon(
|
|
getIconData(iconName),
|
|
color: isSelected
|
|
? colorScheme.onPrimaryContainer
|
|
: colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Common icons available for selection.
|
|
const List<String> availableIcons = [
|
|
'home',
|
|
'settings',
|
|
'search',
|
|
'menu',
|
|
'add',
|
|
'remove',
|
|
'close',
|
|
'check',
|
|
'edit',
|
|
'delete',
|
|
'favorite',
|
|
'star',
|
|
'movie',
|
|
'music_note',
|
|
'photo',
|
|
'videocam',
|
|
'camera',
|
|
'mic',
|
|
'volume_up',
|
|
'play_arrow',
|
|
'pause',
|
|
'stop',
|
|
'skip_next',
|
|
'skip_previous',
|
|
'folder',
|
|
'file_copy',
|
|
'cloud',
|
|
'cloud_download',
|
|
'cloud_upload',
|
|
'download',
|
|
'upload',
|
|
'share',
|
|
'link',
|
|
'public',
|
|
'language',
|
|
'dns',
|
|
'storage',
|
|
'memory',
|
|
'code',
|
|
'terminal',
|
|
'bug_report',
|
|
'api',
|
|
'http',
|
|
'vpn_key',
|
|
'lock',
|
|
'lock_open',
|
|
'security',
|
|
'verified_user',
|
|
'admin_panel_settings',
|
|
'monitor',
|
|
'monitoring',
|
|
'analytics',
|
|
'dashboard',
|
|
'speed',
|
|
'timer',
|
|
'schedule',
|
|
'calendar_today',
|
|
'event',
|
|
'notifications',
|
|
'email',
|
|
'message',
|
|
'chat',
|
|
'forum',
|
|
'person',
|
|
'people',
|
|
'group',
|
|
'account_circle',
|
|
'shopping_cart',
|
|
'credit_card',
|
|
'attach_money',
|
|
'trending_up',
|
|
'trending_down',
|
|
'wifi',
|
|
'bluetooth',
|
|
'router',
|
|
'devices',
|
|
'computer',
|
|
'laptop',
|
|
'phone_android',
|
|
'tablet',
|
|
'watch',
|
|
'tv',
|
|
'games',
|
|
'videogame_asset',
|
|
'sports_esports',
|
|
'local_cafe',
|
|
'local_dining',
|
|
'restaurant',
|
|
'directions_car',
|
|
'flight',
|
|
'hotel',
|
|
'map',
|
|
'place',
|
|
'explore',
|
|
'navigation',
|
|
'book',
|
|
'school',
|
|
'science',
|
|
'psychology',
|
|
'work',
|
|
'business',
|
|
'apartment',
|
|
'location_city',
|
|
'eco',
|
|
'water_drop',
|
|
'air',
|
|
'thermostat',
|
|
'bolt',
|
|
'light_mode',
|
|
'dark_mode',
|
|
'brightness_4',
|
|
'palette',
|
|
'brush',
|
|
'format_paint',
|
|
'extension',
|
|
'widgets',
|
|
'view_module',
|
|
'grid_view',
|
|
'list',
|
|
'table_chart',
|
|
'pie_chart',
|
|
'bar_chart',
|
|
'show_chart',
|
|
'donut_large',
|
|
'disc_full',
|
|
];
|
|
|
|
/// Convert icon name string to IconData.
|
|
IconData getIconData(String iconName) {
|
|
const iconMap = <String, IconData>{
|
|
'home': Icons.home,
|
|
'settings': Icons.settings,
|
|
'search': Icons.search,
|
|
'menu': Icons.menu,
|
|
'add': Icons.add,
|
|
'remove': Icons.remove,
|
|
'close': Icons.close,
|
|
'check': Icons.check,
|
|
'edit': Icons.edit,
|
|
'delete': Icons.delete,
|
|
'favorite': Icons.favorite,
|
|
'star': Icons.star,
|
|
'movie': Icons.movie,
|
|
'music_note': Icons.music_note,
|
|
'photo': Icons.photo,
|
|
'videocam': Icons.videocam,
|
|
'camera': Icons.camera,
|
|
'mic': Icons.mic,
|
|
'volume_up': Icons.volume_up,
|
|
'play_arrow': Icons.play_arrow,
|
|
'pause': Icons.pause,
|
|
'stop': Icons.stop,
|
|
'skip_next': Icons.skip_next,
|
|
'skip_previous': Icons.skip_previous,
|
|
'folder': Icons.folder,
|
|
'file_copy': Icons.file_copy,
|
|
'cloud': Icons.cloud,
|
|
'cloud_download': Icons.cloud_download,
|
|
'cloud_upload': Icons.cloud_upload,
|
|
'download': Icons.download,
|
|
'upload': Icons.upload,
|
|
'share': Icons.share,
|
|
'link': Icons.link,
|
|
'public': Icons.public,
|
|
'language': Icons.language,
|
|
'dns': Icons.dns,
|
|
'storage': Icons.storage,
|
|
'memory': Icons.memory,
|
|
'code': Icons.code,
|
|
'terminal': Icons.terminal,
|
|
'bug_report': Icons.bug_report,
|
|
'api': Icons.api,
|
|
'http': Icons.http,
|
|
'vpn_key': Icons.vpn_key,
|
|
'lock': Icons.lock,
|
|
'lock_open': Icons.lock_open,
|
|
'security': Icons.security,
|
|
'verified_user': Icons.verified_user,
|
|
'admin_panel_settings': Icons.admin_panel_settings,
|
|
'monitor': Icons.monitor,
|
|
'monitoring': Icons.monitor_heart,
|
|
'analytics': Icons.analytics,
|
|
'dashboard': Icons.dashboard,
|
|
'speed': Icons.speed,
|
|
'timer': Icons.timer,
|
|
'schedule': Icons.schedule,
|
|
'calendar_today': Icons.calendar_today,
|
|
'event': Icons.event,
|
|
'notifications': Icons.notifications,
|
|
'email': Icons.email,
|
|
'message': Icons.message,
|
|
'chat': Icons.chat,
|
|
'forum': Icons.forum,
|
|
'person': Icons.person,
|
|
'people': Icons.people,
|
|
'group': Icons.group,
|
|
'account_circle': Icons.account_circle,
|
|
'shopping_cart': Icons.shopping_cart,
|
|
'credit_card': Icons.credit_card,
|
|
'attach_money': Icons.attach_money,
|
|
'trending_up': Icons.trending_up,
|
|
'trending_down': Icons.trending_down,
|
|
'wifi': Icons.wifi,
|
|
'bluetooth': Icons.bluetooth,
|
|
'router': Icons.router,
|
|
'devices': Icons.devices,
|
|
'computer': Icons.computer,
|
|
'laptop': Icons.laptop,
|
|
'phone_android': Icons.phone_android,
|
|
'tablet': Icons.tablet,
|
|
'watch': Icons.watch,
|
|
'tv': Icons.tv,
|
|
'games': Icons.games,
|
|
'videogame_asset': Icons.videogame_asset,
|
|
'sports_esports': Icons.sports_esports,
|
|
'local_cafe': Icons.local_cafe,
|
|
'local_dining': Icons.local_dining,
|
|
'restaurant': Icons.restaurant,
|
|
'directions_car': Icons.directions_car,
|
|
'flight': Icons.flight,
|
|
'hotel': Icons.hotel,
|
|
'map': Icons.map,
|
|
'place': Icons.place,
|
|
'explore': Icons.explore,
|
|
'navigation': Icons.navigation,
|
|
'book': Icons.book,
|
|
'school': Icons.school,
|
|
'science': Icons.science,
|
|
'psychology': Icons.psychology,
|
|
'work': Icons.work,
|
|
'business': Icons.business,
|
|
'apartment': Icons.apartment,
|
|
'location_city': Icons.location_city,
|
|
'eco': Icons.eco,
|
|
'water_drop': Icons.water_drop,
|
|
'air': Icons.air,
|
|
'thermostat': Icons.thermostat,
|
|
'bolt': Icons.bolt,
|
|
'light_mode': Icons.light_mode,
|
|
'dark_mode': Icons.dark_mode,
|
|
'brightness_4': Icons.brightness_4,
|
|
'palette': Icons.palette,
|
|
'brush': Icons.brush,
|
|
'format_paint': Icons.format_paint,
|
|
'extension': Icons.extension,
|
|
'widgets': Icons.widgets,
|
|
'view_module': Icons.view_module,
|
|
'grid_view': Icons.grid_view,
|
|
'list': Icons.list,
|
|
'table_chart': Icons.table_chart,
|
|
'pie_chart': Icons.pie_chart,
|
|
'bar_chart': Icons.bar_chart,
|
|
'show_chart': Icons.show_chart,
|
|
'donut_large': Icons.donut_large,
|
|
'disc_full': Icons.disc_full,
|
|
};
|
|
|
|
return iconMap[iconName] ?? Icons.help_outline;
|
|
}
|