Files
tatlock-ui/lib/shared/widgets/icon_picker.dart
T
Jeroen SchweitzerandClaude Opus 4.5 327b4a1233 feat(front-hall): add link management with form and icon picker
- 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>
2026-01-02 23:19:03 +01:00

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;
}