From 327b4a1233a95420defa36712d1a83e40fb2d455 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 2 Jan 2026 23:19:03 +0100 Subject: [PATCH] feat(front-hall): add link management with form and icon picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../presentation/widgets/quick_link_form.dart | 334 +++++++++++++ .../widgets/quick_link_settings_content.dart | 175 +++---- .../widgets/quick_links_panel.dart | 28 +- lib/shared/widgets/icon_picker.dart | 465 ++++++++++++++++++ lib/shared/widgets/widgets.dart | 1 + 5 files changed, 859 insertions(+), 144 deletions(-) create mode 100644 lib/features/front_hall/presentation/widgets/quick_link_form.dart create mode 100644 lib/shared/widgets/icon_picker.dart diff --git a/lib/features/front_hall/presentation/widgets/quick_link_form.dart b/lib/features/front_hall/presentation/widgets/quick_link_form.dart new file mode 100644 index 0000000..96a3409 --- /dev/null +++ b/lib/features/front_hall/presentation/widgets/quick_link_form.dart @@ -0,0 +1,334 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tatlock_ui/features/front_hall/domain/entities/quick_link.dart'; +import 'package:tatlock_ui/features/front_hall/presentation/providers/quick_links_provider.dart'; +import 'package:tatlock_ui/shared/widgets/entity_page.dart'; +import 'package:tatlock_ui/shared/widgets/icon_picker.dart'; + +/// Form for creating and editing Quick Links. +/// +/// Handles validation, submission, and state management for link data. +class QuickLinkForm extends ConsumerStatefulWidget { + const QuickLinkForm({ + super.key, + required this.mode, + this.quickLink, + required this.onCancel, + required this.onSaved, + }); + + final EntityPageMode mode; + final QuickLink? quickLink; + final VoidCallback onCancel; + final VoidCallback onSaved; + + @override + ConsumerState createState() => _QuickLinkFormState(); +} + +class _QuickLinkFormState extends ConsumerState { + final _formKey = GlobalKey(); + + late final TextEditingController _nameController; + late final TextEditingController _urlController; + late final TextEditingController _categoryController; + late String _iconName; + late QuickLinkType _type; + late bool _isActive; + + bool _isSubmitting = false; + + @override + void initState() { + super.initState(); + final link = widget.quickLink; + _nameController = TextEditingController(text: link?.name ?? ''); + _urlController = TextEditingController(text: link?.url ?? ''); + _categoryController = TextEditingController(text: link?.category ?? ''); + _iconName = link?.iconName ?? 'link'; + _type = link?.type ?? QuickLinkType.iframe; + _isActive = link?.isActive ?? true; + } + + @override + void dispose() { + _nameController.dispose(); + _urlController.dispose(); + _categoryController.dispose(); + super.dispose(); + } + + bool get isCreate => widget.mode == EntityPageMode.create; + + Future _handleSubmit() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isSubmitting = true); + + try { + final actions = ref.read(quickLinkActionsProvider.notifier); + + // Generate ID for new links + final id = widget.quickLink?.id ?? + _nameController.text.toLowerCase().replaceAll(RegExp(r'\s+'), '-'); + + final link = QuickLink( + id: id, + name: _nameController.text.trim(), + url: _urlController.text.trim(), + iconName: _iconName, + category: + _categoryController.text.trim().isEmpty ? null : _categoryController.text.trim(), + type: _type, + isActive: _isActive, + sortOrder: widget.quickLink?.sortOrder ?? 0, + ); + + if (isCreate) { + await actions.create(link); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Link created successfully'), + behavior: SnackBarBehavior.floating, + ), + ); + } + } else { + await actions.update(link); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Link updated successfully'), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + widget.onSaved(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + behavior: SnackBarBehavior.floating, + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } finally { + if (mounted) { + setState(() => _isSubmitting = false); + } + } + } + + Future _handleDelete() async { + if (widget.quickLink == null) return; + + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Link'), + content: Text('Are you sure you want to delete "${widget.quickLink!.name}"?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + ), + child: const Text('Delete'), + ), + ], + ), + ); + + if (confirm == true) { + setState(() => _isSubmitting = true); + try { + await ref.read(quickLinkActionsProvider.notifier).delete(widget.quickLink!.id); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Link deleted'), + behavior: SnackBarBehavior.floating, + ), + ); + widget.onSaved(); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error deleting: $e'), + behavior: SnackBarBehavior.floating, + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } finally { + if (mounted) { + setState(() => _isSubmitting = false); + } + } + } + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(24), + children: [ + // Name field + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + hintText: 'Enter link name', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Name is required'; + } + return null; + }, + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 16), + + // URL field + TextFormField( + controller: _urlController, + decoration: const InputDecoration( + labelText: 'URL', + hintText: 'https://example.com', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'URL is required'; + } + final uri = Uri.tryParse(value); + if (uri == null || !uri.hasScheme) { + return 'Enter a valid URL with scheme (http/https)'; + } + return null; + }, + keyboardType: TextInputType.url, + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 16), + + // Category field + TextFormField( + controller: _categoryController, + decoration: const InputDecoration( + labelText: 'Category', + hintText: 'e.g., Home, Infrastructure, Coding', + border: OutlineInputBorder(), + ), + textInputAction: TextInputAction.done, + ), + const SizedBox(height: 24), + + // Icon picker + IconPicker( + selectedIcon: _iconName, + onChanged: (icon) => setState(() => _iconName = icon), + ), + const SizedBox(height: 24), + + // Link type + Text( + 'Link Type', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + SegmentedButton( + segments: const [ + ButtonSegment( + value: QuickLinkType.iframe, + icon: Icon(Icons.web), + label: Text('Iframe'), + ), + ButtonSegment( + value: QuickLinkType.newTab, + icon: Icon(Icons.open_in_new), + label: Text('New Tab'), + ), + ], + selected: {_type}, + onSelectionChanged: (selected) { + setState(() => _type = selected.first); + }, + ), + const SizedBox(height: 8), + Text( + _type == QuickLinkType.iframe + ? 'Opens embedded in the dashboard' + : 'Opens in a new browser tab', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.outline, + ), + ), + const SizedBox(height: 24), + + // Active toggle + SwitchListTile( + title: const Text('Active'), + subtitle: const Text('Show this link in the panel'), + value: _isActive, + onChanged: (value) => setState(() => _isActive = value), + contentPadding: EdgeInsets.zero, + ), + const SizedBox(height: 32), + + // Action buttons + Row( + children: [ + if (!isCreate) ...[ + OutlinedButton.icon( + onPressed: _isSubmitting ? null : _handleDelete, + style: OutlinedButton.styleFrom( + foregroundColor: colorScheme.error, + ), + icon: const Icon(Icons.delete), + label: const Text('Delete'), + ), + const Spacer(), + ] else + const Spacer(), + TextButton( + onPressed: _isSubmitting ? null : widget.onCancel, + child: const Text('Cancel'), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: _isSubmitting ? null : _handleSubmit, + child: _isSubmitting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(isCreate ? 'Create' : 'Save'), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/front_hall/presentation/widgets/quick_link_settings_content.dart b/lib/features/front_hall/presentation/widgets/quick_link_settings_content.dart index 68a8a4a..a7d5e7e 100644 --- a/lib/features/front_hall/presentation/widgets/quick_link_settings_content.dart +++ b/lib/features/front_hall/presentation/widgets/quick_link_settings_content.dart @@ -2,24 +2,62 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tatlock_ui/features/front_hall/presentation/providers/front_hall_state_provider.dart'; import 'package:tatlock_ui/features/front_hall/presentation/providers/quick_links_provider.dart'; +import 'package:tatlock_ui/features/front_hall/presentation/widgets/quick_link_form.dart'; +import 'package:tatlock_ui/shared/widgets/entity_page.dart'; /// Settings content for Quick Link management. /// /// Shows either: -/// - Link editor form (when a link is selected) +/// - Create form (when isCreating state is set) +/// - Edit form (when a link is selected) /// - Empty state with "Add New Link" prompt (when no link selected) -/// -/// Full implementation in Phase 4. -class QuickLinkSettingsContent extends ConsumerWidget { +class QuickLinkSettingsContent extends ConsumerStatefulWidget { const QuickLinkSettingsContent({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => + _QuickLinkSettingsContentState(); +} + +class _QuickLinkSettingsContentState + extends ConsumerState { + bool _isCreating = false; + + void _startCreating() { + setState(() => _isCreating = true); + // Clear any selected link when creating new + ref.read(frontHallStateProvider.notifier).clearSelectedLink(); + } + + void _stopCreating() { + setState(() => _isCreating = false); + } + + @override + Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; final frontHallState = ref.watch(frontHallStateProvider); final selectedLinkId = frontHallState.selectedLinkId; + // Create mode - show create form + if (_isCreating) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: _stopCreating, + ), + title: const Text('Create New Link'), + ), + body: QuickLinkForm( + mode: EntityPageMode.create, + onCancel: _stopCreating, + onSaved: _stopCreating, + ), + ); + } + // No link selected - show empty state if (selectedLinkId == null) { return Center( @@ -42,7 +80,7 @@ class QuickLinkSettingsContent extends ConsumerWidget { ), const SizedBox(height: 8), Text( - 'Select a link from the panel to edit, or click "Add New Link" to create one.', + 'Select a link from the panel to edit, or create a new one.', textAlign: TextAlign.center, style: textTheme.bodyMedium?.copyWith( color: colorScheme.outline, @@ -50,15 +88,7 @@ class QuickLinkSettingsContent extends ConsumerWidget { ), const SizedBox(height: 24), FilledButton.icon( - onPressed: () { - // TODO: Create new link - will show form in Phase 4 - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Link editor coming in Phase 4'), - behavior: SnackBarBehavior.floating, - ), - ); - }, + onPressed: _startCreating, icon: const Icon(Icons.add), label: const Text('Create New Link'), ), @@ -68,7 +98,7 @@ class QuickLinkSettingsContent extends ConsumerWidget { ); } - // Link selected - show placeholder for editor (Phase 4) + // Link selected - show edit form final linkAsync = ref.watch(quickLinkProvider(selectedLinkId)); return linkAsync.when( @@ -82,7 +112,8 @@ class QuickLinkSettingsContent extends ConsumerWidget { Text('Error loading link: $error'), const SizedBox(height: 16), TextButton.icon( - onPressed: () => ref.invalidate(quickLinkProvider(selectedLinkId)), + onPressed: () => + ref.invalidate(quickLinkProvider(selectedLinkId)), icon: const Icon(Icons.refresh), label: const Text('Retry'), ), @@ -98,110 +129,18 @@ class QuickLinkSettingsContent extends ConsumerWidget { .clearSelectedLink(), ), title: Text('Edit: ${link.name}'), - actions: [ - TextButton( - onPressed: () { - // TODO: Save link - Phase 4 - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Save functionality coming in Phase 4'), - behavior: SnackBarBehavior.floating, - ), - ); - }, - child: const Text('Save'), - ), - ], ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Placeholder form fields (Phase 4) - _PlaceholderField(label: 'Name', value: link.name), - const SizedBox(height: 16), - _PlaceholderField(label: 'URL', value: link.url), - const SizedBox(height: 16), - _PlaceholderField( - label: 'Category', - value: link.category ?? 'None', - ), - const SizedBox(height: 16), - _PlaceholderField( - label: 'Type', - value: link.isIframe ? 'Iframe' : 'New Tab', - ), - const SizedBox(height: 16), - _PlaceholderField(label: 'Icon', value: link.iconName), - const SizedBox(height: 32), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon(Icons.info_outline, color: colorScheme.primary), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Full link editor form will be implemented in Phase 4.', - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - ), - ], - ), + body: QuickLinkForm( + mode: EntityPageMode.edit, + quickLink: link, + onCancel: () => ref + .read(frontHallStateProvider.notifier) + .clearSelectedLink(), + onSaved: () => ref + .read(frontHallStateProvider.notifier) + .clearSelectedLink(), ), ), ); } } - -class _PlaceholderField extends StatelessWidget { - const _PlaceholderField({ - required this.label, - required this.value, - }); - - final String label; - final String value; - - @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: 4), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: colorScheme.outlineVariant), - ), - child: Text( - value, - style: textTheme.bodyLarge, - ), - ), - ], - ); - } -} diff --git a/lib/features/front_hall/presentation/widgets/quick_links_panel.dart b/lib/features/front_hall/presentation/widgets/quick_links_panel.dart index f86c189..2dacef3 100644 --- a/lib/features/front_hall/presentation/widgets/quick_links_panel.dart +++ b/lib/features/front_hall/presentation/widgets/quick_links_panel.dart @@ -4,6 +4,7 @@ import 'package:tatlock_ui/features/front_hall/domain/entities/quick_link.dart'; import 'package:tatlock_ui/features/front_hall/presentation/providers/front_hall_state_provider.dart'; import 'package:tatlock_ui/features/front_hall/presentation/providers/quick_links_provider.dart'; import 'package:tatlock_ui/shared/layouts/widgets/panel_header.dart'; +import 'package:tatlock_ui/shared/widgets/icon_picker.dart'; import 'package:url_launcher/url_launcher.dart'; /// Left-side panel for quick link navigation in Front Hall. @@ -332,31 +333,6 @@ class _QuickLinkTile extends StatelessWidget { final VoidCallback? onEdit; final VoidCallback? onDelete; - IconData _getIconData(String iconName) { - // Map common icon names to Material icons - return switch (iconName.toLowerCase()) { - 'home' => Icons.home, - 'movie' => Icons.movie, - 'chat' => Icons.chat, - 'search' => Icons.search, - 'videogame_asset' => Icons.videogame_asset, - 'code' => Icons.code, - 'terminal' => Icons.terminal, - 'monitoring' => Icons.show_chart, - 'dns' => Icons.dns, - 'public' => Icons.public, - 'api' => Icons.api, - 'settings' => Icons.settings, - 'storage' => Icons.storage, - 'cloud' => Icons.cloud, - 'security' => Icons.security, - 'person' => Icons.person, - 'folder' => Icons.folder, - 'link' => Icons.link, - _ => Icons.link, - }; - } - @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; @@ -377,7 +353,7 @@ class _QuickLinkTile extends StatelessWidget { child: Row( children: [ Icon( - _getIconData(link.iconName), + getIconData(link.iconName), size: 20, color: isSelected ? colorScheme.primary diff --git a/lib/shared/widgets/icon_picker.dart b/lib/shared/widgets/icon_picker.dart new file mode 100644 index 0000000..a0d5ada --- /dev/null +++ b/lib/shared/widgets/icon_picker.dart @@ -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 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 _showIconPicker(BuildContext context) async { + final result = await showDialog( + 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 onSelected; + + @override + State createState() => _IconPickerDialogState(); +} + +class _IconPickerDialogState extends State { + String _searchQuery = ''; + + List 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 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 = { + '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; +} diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart index 27c89f9..a010639 100644 --- a/lib/shared/widgets/widgets.dart +++ b/lib/shared/widgets/widgets.dart @@ -3,4 +3,5 @@ library; export 'air_quality_widget.dart'; export 'gauge_widget.dart'; +export 'icon_picker.dart'; export 'weather_widget.dart';