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>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
9c9ec472ef
commit
327b4a1233
@@ -0,0 +1,465 @@
|
||||
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;
|
||||
}
|
||||
@@ -3,4 +3,5 @@ library;
|
||||
|
||||
export 'air_quality_widget.dart';
|
||||
export 'gauge_widget.dart';
|
||||
export 'icon_picker.dart';
|
||||
export 'weather_widget.dart';
|
||||
|
||||
Reference in New Issue
Block a user