diff --git a/lib/const/settings.dart b/lib/const/settings.dart index 3108727b..9af563c0 100644 --- a/lib/const/settings.dart +++ b/lib/const/settings.dart @@ -216,6 +216,16 @@ class Settings { destructiveForeground: Color(0xfffafafa), ); + // Semantic accents for settings tile icons. Each hue follows the meaning of + // its setting (never the violet action hue, which is reserved for + // commands/selection). + static const Color settingsAgentAccent = Color(0xff5da37e); // team markers + static const Color settingsAbilityAccent = Color(0xff6aa1d8); // utility + static const Color settingsNeutralAccent = Color(0xffa1a1aa); // greys toggle + static const Color settingsPersistenceAccent = Color(0xff4b8f86); // saving + static const Color settingsDiscordAccent = Color(0xff5865f2); // brand blurple + static const Color settingsMapAccent = Color(0xffb27c40); // map layers + static const cardForegroundBackdrop = BoxShadow( color: Colors.black54, // High opacity because the background is dark blurRadius: 12, diff --git a/lib/interactive_map.dart b/lib/interactive_map.dart index 9125c639..b391d642 100644 --- a/lib/interactive_map.dart +++ b/lib/interactive_map.dart @@ -23,31 +23,9 @@ import 'package:icarus/widgets/lineup_control_buttons.dart'; import 'package:icarus/widgets/page_transition_overlay.dart'; import 'package:icarus/widgets/image_drop_target.dart'; import 'package:icarus/widgets/line_up_placer.dart'; +import 'package:icarus/widgets/map_svg_color_mapper.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; -class _MapSvgColorMapper extends ColorMapper { - const _MapSvgColorMapper(this.replacements); - - final Map replacements; - - @override - Color substitute( - String? id, - String elementName, - String attributeName, - Color color, - ) { - final opaqueColorValue = (color.toARGB32() & 0x00FFFFFF) | 0xFF000000; - final replacement = replacements[opaqueColorValue]; - if (replacement == null) { - return color; - } - // Keep per-element opacity from the original SVG. - final alpha = (color.a * 255.0).round().clamp(0, 255); - return replacement.withAlpha(alpha); - } -} - class InteractiveMap extends ConsumerStatefulWidget { const InteractiveMap({ super.key, @@ -58,10 +36,6 @@ class InteractiveMap extends ConsumerStatefulWidget { } class _InteractiveMapState extends ConsumerState { - static const Color _mapBaseSourceColor = Color(0xFF271406); - static const Color _mapDetailSourceColor = Color(0xFFB27C40); - static const Color _mapHighlightSourceColor = Color(0xFFF08234); - final controller = TransformationController(); Size? _lastViewportSize; Size? _lastPlayAreaSize; @@ -153,11 +127,7 @@ class _InteractiveMapState extends ConsumerState { ), ); final effectivePalette = ref.watch(effectiveMapThemePaletteProvider); - final mapColorMapper = _MapSvgColorMapper({ - _mapBaseSourceColor.toARGB32(): effectivePalette.baseColor, - _mapDetailSourceColor.toARGB32(): effectivePalette.detailColor, - _mapHighlightSourceColor.toARGB32(): effectivePalette.highlightColor, - }); + final mapColorMapper = MapSvgColorMapper.forPalette(effectivePalette); String assetName = 'assets/maps/${Maps.mapNames[ref.watch(mapProvider).currentMap]}_map${isAttack ? "" : "_defense"}.svg'; diff --git a/lib/providers/user_preferences_provider.dart b/lib/providers/user_preferences_provider.dart index 5cc8f5a9..e6baf463 100644 --- a/lib/providers/user_preferences_provider.dart +++ b/lib/providers/user_preferences_provider.dart @@ -199,8 +199,8 @@ class AppPreferences extends HiveObject { drawingThickness: drawingThickness ?? this.drawingThickness, discordPresenceEnabled: discordPresenceEnabled ?? this.discordPresenceEnabled, - videoExportStepDurationSeconds: videoExportStepDurationSeconds ?? - this.videoExportStepDurationSeconds, + videoExportStepDurationSeconds: + videoExportStepDurationSeconds ?? this.videoExportStepDurationSeconds, ); } } @@ -390,36 +390,41 @@ class MapThemeProfilesProvider extends Notifier { return profile; } - Future renameProfile({ + /// Returns false when nothing was written (unknown or built-in profile, + /// or an empty name) so callers never report a success that didn't happen. + Future renameProfile({ required String profileId, required String newName, }) async { final profile = _findProfile(profileId); if (profile == null || profile.isBuiltIn) { - return; + return false; } final trimmed = newName.trim(); if (trimmed.isEmpty) { - return; + return false; } final updated = profile.copyWith(name: trimmed); await Hive.box(HiveBoxNames.mapThemeProfilesBox) .put(updated.id, updated); await refreshFromHive(); + return true; } - Future updateProfilePalette({ + /// Returns false when nothing was written (unknown or built-in profile). + Future updateProfilePalette({ required String profileId, required MapThemePalette palette, }) async { final profile = _findProfile(profileId); if (profile == null || profile.isBuiltIn) { - return; + return false; } final updated = profile.copyWith(palette: palette); await Hive.box(HiveBoxNames.mapThemeProfilesBox) .put(updated.id, updated); await refreshFromHive(); + return true; } Future deleteProfile(String profileId) async { diff --git a/lib/screenshot/screenshot_view.dart b/lib/screenshot/screenshot_view.dart index eed069be..ecf419c4 100644 --- a/lib/screenshot/screenshot_view.dart +++ b/lib/screenshot/screenshot_view.dart @@ -19,31 +19,10 @@ import 'package:icarus/providers/strategy_settings_provider.dart'; import 'package:icarus/providers/text_provider.dart'; import 'package:icarus/providers/utility_provider.dart'; import 'package:icarus/widgets/dot_painter.dart'; +import 'package:icarus/widgets/map_svg_color_mapper.dart'; import 'package:icarus/widgets/draggable_widgets/placed_widget_builder.dart'; import 'package:icarus/widgets/drawing_painter.dart'; -class _MapSvgColorMapper extends ColorMapper { - const _MapSvgColorMapper(this.replacements); - - final Map replacements; - - @override - Color substitute( - String? id, - String elementName, - String attributeName, - Color color, - ) { - final opaqueColorValue = (color.toARGB32() & 0x00FFFFFF) | 0xFF000000; - final replacement = replacements[opaqueColorValue]; - if (replacement == null) { - return color; - } - final alpha = (color.a * 255.0).round().clamp(0, 255); - return replacement.withAlpha(alpha); - } -} - class ScreenshotView extends ConsumerWidget { ScreenshotView({ super.key, @@ -129,11 +108,7 @@ class ScreenshotView extends ConsumerWidget { String ultOrbsAssetName = 'assets/maps/${Maps.mapNames[ref.watch(mapProvider).currentMap]}_ult_orbs.svg'; final effectivePalette = ref.watch(effectiveMapThemePaletteProvider); - final mapColorMapper = _MapSvgColorMapper({ - 0xFF271406: effectivePalette.baseColor, - 0xFFB27C40: effectivePalette.detailColor, - 0xFFF08234: effectivePalette.highlightColor, - }); + final mapColorMapper = MapSvgColorMapper.forPalette(effectivePalette); final mapWidth = CoordinateSystem.screenShotSize.height * CoordinateSystem.instance.mapAspectRatio; final mapLeft = (CoordinateSystem.screenShotSize.width - mapWidth) / 2; diff --git a/lib/widgets/dialogs/map_theme_editor_dialog.dart b/lib/widgets/dialogs/map_theme_editor_dialog.dart new file mode 100644 index 00000000..f9dba277 --- /dev/null +++ b/lib/widgets/dialogs/map_theme_editor_dialog.dart @@ -0,0 +1,442 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:icarus/const/maps.dart'; +import 'package:icarus/const/settings.dart'; +import 'package:icarus/providers/map_provider.dart'; +import 'package:icarus/providers/strategy_provider.dart'; +import 'package:icarus/providers/user_preferences_provider.dart'; +import 'package:icarus/widgets/better_color_picker.dart'; +import 'package:icarus/widgets/custom_text_field.dart'; +import 'package:icarus/widgets/dot_painter.dart'; +import 'package:icarus/widgets/icarus_color_picker_style.dart'; +import 'package:icarus/widgets/map_svg_color_mapper.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +enum MapThemeEditorMode { + /// Design a new profile from scratch. + createProfile, + + /// Change an existing profile everywhere it is used. + editProfile, + + /// One-off palette for the open strategy only. + customizeStrategy, +} + +Future showMapThemeEditorDialog( + BuildContext context, { + required MapThemeEditorMode mode, + required MapThemePalette initialPalette, + MapThemeProfile? profile, +}) { + return showShadDialog( + context: context, + builder: (context) => MapThemeEditorDialog( + mode: mode, + initialPalette: initialPalette, + profile: profile, + ), + ); +} + +class MapThemeEditorDialog extends ConsumerStatefulWidget { + const MapThemeEditorDialog({ + super.key, + required this.mode, + required this.initialPalette, + this.profile, + }) : assert( + mode != MapThemeEditorMode.editProfile || profile != null, + 'editProfile requires the profile being edited', + ); + + final MapThemeEditorMode mode; + final MapThemePalette initialPalette; + final MapThemeProfile? profile; + + @override + ConsumerState createState() => + _MapThemeEditorDialogState(); +} + +enum _PaletteSlot { base, detail, highlight } + +class _MapThemeEditorDialogState extends ConsumerState { + late MapThemePalette _palette = widget.initialPalette; + late final TextEditingController _nameController = TextEditingController(); + _PaletteSlot _selectedSlot = _PaletteSlot.base; + + @override + void initState() { + super.initState(); + switch (widget.mode) { + case MapThemeEditorMode.createProfile: + final profiles = ref.read(mapThemeProfilesProvider).profiles; + _nameController.text = + "Profile ${MapThemeProfilesProvider.nextGeneratedProfileNumber( + profiles.where((p) => !p.isBuiltIn).toList(), + )}"; + case MapThemeEditorMode.editProfile: + _nameController.text = widget.profile!.name; + case MapThemeEditorMode.customizeStrategy: + break; + } + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.sizeOf(context); + final dialogWidth = (screenSize.width - 96).clamp(560.0, 1120.0); + final dialogHeight = (screenSize.height - 80).clamp(420.0, 780.0); + + final mapState = ref.watch(mapProvider); + final mapAsset = + 'assets/maps/${Maps.mapNames[mapState.currentMap]}_map${mapState.isAttack ? "" : "_defense"}.svg'; + + return ShadDialog( + constraints: BoxConstraints( + maxWidth: dialogWidth, + maxHeight: dialogHeight, + ), + padding: EdgeInsets.zero, + scrollable: false, + closeIconPosition: const ShadPosition(top: 14, right: 14), + child: Material( + color: Colors.transparent, + child: SizedBox( + width: dialogWidth, + height: dialogHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(child: _buildPreview(mapAsset)), + Container( + width: 1, + color: + Settings.tacticalVioletTheme.border.withValues(alpha: 0.9), + ), + SizedBox(width: 340, child: _buildControls(context)), + ], + ), + ), + ), + ); + } + + Widget _buildPreview(String mapAsset) { + return Container( + decoration: BoxDecoration( + gradient: RadialGradient( + center: Alignment.center, + radius: 1.5, + colors: [ + Settings.tacticalVioletTheme.card, + Settings.tacticalVioletTheme.background, + ], + ), + ), + child: Stack( + children: [ + const Positioned.fill( + child: Padding( + padding: EdgeInsets.all(4), + child: DotGrid(), + ), + ), + Positioned.fill( + child: Padding( + padding: const EdgeInsets.all(12), + child: SvgPicture.asset( + mapAsset, + colorMapper: MapSvgColorMapper.forPalette(_palette), + fit: BoxFit.contain, + semanticsLabel: 'Map theme preview', + ), + ), + ), + ], + ), + ); + } + + Widget _buildControls(BuildContext context) { + final title = switch (widget.mode) { + MapThemeEditorMode.createProfile => "New profile", + MapThemeEditorMode.editProfile => "Edit profile", + MapThemeEditorMode.customizeStrategy => "Customize map theme", + }; + final showNameField = widget.mode != MapThemeEditorMode.customizeStrategy; + + return Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(right: 28), + child: Text( + title, + style: ShadTheme.of(context).textTheme.p.copyWith( + color: Settings.tacticalVioletTheme.foreground, + fontSize: 17, + fontWeight: FontWeight.w500, + ), + ), + ), + if (widget.mode == MapThemeEditorMode.customizeStrategy) ...[ + const SizedBox(height: 4), + Text( + "Only this strategy is affected.", + style: ShadTheme.of(context).textTheme.small.copyWith( + color: Settings.tacticalVioletTheme.mutedForeground, + ), + ), + ], + const SizedBox(height: 14), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showNameField) ...[ + CustomTextField( + controller: _nameController, + hintText: "Profile name", + ), + const SizedBox(height: 14), + ], + _buildSlotTabs(context), + const SizedBox(height: 12), + BetterColorPicker( + key: ValueKey(_selectedSlot), + value: _selectedSlotColor, + initialMode: BetterColorPickerMode.hsv, + style: icarusColorPickerStyle, + onChanging: (next) => _updateSlot(_selectedSlot, next), + onChanged: (next) => _updateSlot(_selectedSlot, next), + ), + ], + ), + ), + ), + const SizedBox(height: 12), + _buildFooter(context), + ], + ), + ); + } + + Widget _buildSlotTabs(BuildContext context) { + const theme = Settings.tacticalVioletTheme; + + return FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Container( + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: theme.secondary.withValues(alpha: 0.35), + borderRadius: BorderRadius.circular(9), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildSlotTab(context, _PaletteSlot.base, "Base"), + const SizedBox(width: 2), + _buildSlotTab(context, _PaletteSlot.detail, "Detail"), + const SizedBox(width: 2), + _buildSlotTab(context, _PaletteSlot.highlight, "Highlight"), + ], + ), + ), + ); + } + + Widget _buildSlotTab(BuildContext context, _PaletteSlot slot, String label) { + final isSelected = _selectedSlot == slot; + const theme = Settings.tacticalVioletTheme; + + return Material( + type: MaterialType.transparency, + child: InkWell( + onTap: () => setState(() => _selectedSlot = slot), + mouseCursor: SystemMouseCursors.click, + borderRadius: BorderRadius.circular(6), + hoverColor: theme.secondary.withValues(alpha: 0.45), + splashFactory: NoSplash.splashFactory, + child: Ink( + decoration: BoxDecoration( + color: isSelected + ? theme.secondary.withValues(alpha: 0.95) + : Colors.transparent, + borderRadius: BorderRadius.circular(6), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: _slotColor(slot), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: theme.border), + ), + ), + const SizedBox(width: 7), + Text( + label, + style: ShadTheme.of(context).textTheme.small.copyWith( + color: isSelected + ? theme.foreground + : theme.mutedForeground, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Color get _selectedSlotColor => _slotColor(_selectedSlot); + + Color _slotColor(_PaletteSlot slot) { + return switch (slot) { + _PaletteSlot.base => _palette.baseColor, + _PaletteSlot.detail => _palette.detailColor, + _PaletteSlot.highlight => _palette.highlightColor, + }; + } + + void _updateSlot(_PaletteSlot slot, Color color) { + setState(() { + _palette = switch (slot) { + _PaletteSlot.base => + _palette.copyWith(baseColorValue: color.toARGB32()), + _PaletteSlot.detail => + _palette.copyWith(detailColorValue: color.toARGB32()), + _PaletteSlot.highlight => + _palette.copyWith(highlightColorValue: color.toARGB32()), + }; + }); + } + + Widget _buildFooter(BuildContext context) { + return FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () => Navigator.of(context).pop(), + child: const Text("Cancel"), + ), + const SizedBox(width: 6), + switch (widget.mode) { + MapThemeEditorMode.createProfile => ShadButton( + size: ShadButtonSize.sm, + onPressed: _saveNewProfile, + child: const Text("Save profile"), + ), + MapThemeEditorMode.editProfile => ShadButton( + size: ShadButtonSize.sm, + onPressed: _saveProfileEdits, + child: const Text("Save changes"), + ), + MapThemeEditorMode.customizeStrategy => ShadButton( + size: ShadButtonSize.sm, + onPressed: _applyToStrategy, + child: const Text("Apply"), + ), + }, + ], + ), + ); + } + + Future _saveNewProfile() async { + final created = await ref + .read(mapThemeProfilesProvider.notifier) + .createProfile(name: _nameController.text.trim(), palette: _palette); + if (created == null) { + Settings.showToast( + message: "Profile limit reached or invalid name.", + backgroundColor: Settings.tacticalVioletTheme.destructive, + ); + return; + } + final hasActiveStrategy = ref.read(strategyProvider).stratName != null; + if (hasActiveStrategy) { + ref + .read(strategyProvider.notifier) + .setThemeProfileForCurrentStrategy(created.id); + } + if (!mounted) return; + Navigator.of(context).pop(); + Settings.showToast( + message: + hasActiveStrategy ? "Profile saved and applied." : "Profile saved.", + backgroundColor: Settings.tacticalVioletTheme.primary, + ); + } + + Future _saveProfileEdits() async { + final profile = widget.profile!; + final trimmedName = _nameController.text.trim(); + if (trimmedName.isEmpty) { + Settings.showToast( + message: "Enter a profile name.", + backgroundColor: Settings.tacticalVioletTheme.destructive, + ); + return; + } + final notifier = ref.read(mapThemeProfilesProvider.notifier); + final paletteSaved = await notifier.updateProfilePalette( + profileId: profile.id, + palette: _palette, + ); + if (!paletteSaved) { + Settings.showToast( + message: "Couldn't update this profile.", + backgroundColor: Settings.tacticalVioletTheme.destructive, + ); + return; + } + var renamed = true; + if (trimmedName != profile.name) { + renamed = await notifier.renameProfile( + profileId: profile.id, + newName: trimmedName, + ); + } + if (!mounted) return; + Navigator.of(context).pop(); + Settings.showToast( + message: renamed + ? "Profile updated." + : "Colors saved, but the name couldn't be changed.", + backgroundColor: renamed + ? Settings.tacticalVioletTheme.primary + : Settings.tacticalVioletTheme.destructive, + ); + } + + void _applyToStrategy() { + ref + .read(strategyProvider.notifier) + .setThemeOverrideForCurrentStrategy(_palette); + Navigator.of(context).pop(); + } +} diff --git a/lib/widgets/map_svg_color_mapper.dart b/lib/widgets/map_svg_color_mapper.dart new file mode 100644 index 00000000..e92c3598 --- /dev/null +++ b/lib/widgets/map_svg_color_mapper.dart @@ -0,0 +1,41 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:icarus/providers/user_preferences_provider.dart'; + +/// The three placeholder colors the map SVGs are authored with. Rendering +/// swaps them for a strategy's palette via [MapSvgColorMapper]. +class MapSvgSourceColors { + static const Color base = Color(0xFF271406); + static const Color detail = Color(0xFFB27C40); + static const Color highlight = Color(0xFFF08234); +} + +class MapSvgColorMapper extends ColorMapper { + const MapSvgColorMapper(this.replacements); + + MapSvgColorMapper.forPalette(MapThemePalette palette) + : replacements = { + MapSvgSourceColors.base.toARGB32(): palette.baseColor, + MapSvgSourceColors.detail.toARGB32(): palette.detailColor, + MapSvgSourceColors.highlight.toARGB32(): palette.highlightColor, + }; + + final Map replacements; + + @override + Color substitute( + String? id, + String elementName, + String attributeName, + Color color, + ) { + final opaqueColorValue = (color.toARGB32() & 0x00FFFFFF) | 0xFF000000; + final replacement = replacements[opaqueColorValue]; + if (replacement == null) { + return color; + } + // Keep per-element opacity from the original SVG. + final alpha = (color.a * 255.0).round().clamp(0, 255); + return replacement.withAlpha(alpha); + } +} diff --git a/lib/widgets/map_theme_settings_section.dart b/lib/widgets/map_theme_settings_section.dart index ef0b7977..29a41d93 100644 --- a/lib/widgets/map_theme_settings_section.dart +++ b/lib/widgets/map_theme_settings_section.dart @@ -1,445 +1,242 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:icarus/widgets/better_color_picker.dart'; import 'package:icarus/const/settings.dart'; -import 'package:icarus/providers/user_preferences_provider.dart'; import 'package:icarus/providers/strategy_provider.dart'; +import 'package:icarus/providers/user_preferences_provider.dart'; import 'package:icarus/widgets/custom_text_field.dart'; -import 'package:icarus/widgets/icarus_color_picker_style.dart'; +import 'package:icarus/widgets/dialogs/confirm_alert_dialog.dart'; +import 'package:icarus/widgets/dialogs/map_theme_editor_dialog.dart'; import 'package:icarus/widgets/settings_scope_card.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; -enum MapThemeSettingsScope { - strategy, - global, -} - -class MapThemeSettingsSection extends StatelessWidget { - const MapThemeSettingsSection({ - super.key, - required this.scope, - this.onManageProfiles, - }); - - final MapThemeSettingsScope scope; - final VoidCallback? onManageProfiles; - - @override - Widget build(BuildContext context) { - final isStrategyScope = scope == MapThemeSettingsScope.strategy; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsScopeCard( - title: isStrategyScope ? "Strategy map theme" : "Theme profiles", - description: isStrategyScope - ? "Choose the color profile for this strategy." - : "Manage saved map color profiles and choose the default.", - child: _ThemeProfilesSection( - scope: scope, - onManageProfiles: onManageProfiles, - ), - ), - ], - ); - } -} - -class _ThemeProfilesSection extends StatelessWidget { - const _ThemeProfilesSection({ - required this.scope, - this.onManageProfiles, - }); - - final MapThemeSettingsScope scope; - final VoidCallback? onManageProfiles; +/// The single home for map themes: pick the open strategy's theme, manage +/// profiles, and jump into the live editor. Every path routes through +/// [showMapThemeEditorDialog]. +class MapThemeSettingsSection extends ConsumerWidget { + const MapThemeSettingsSection({super.key}); @override - Widget build(BuildContext context) { - final isStrategyScope = scope == MapThemeSettingsScope.strategy; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isStrategyScope) ...[ - const _ActiveThemeCard(), - const SizedBox(height: 12), - ], - _ProfileLibrarySection( - scope: scope, - onManageProfiles: onManageProfiles, - ), - ], + Widget build(BuildContext context, WidgetRef ref) { + final customCount = ref + .watch(mapThemeProfilesProvider) + .profiles + .where((p) => !p.isBuiltIn) + .length; + + return SettingsScopeCard( + title: "Map theme", + trailing: Text( + "$customCount/${MapThemeProfilesProvider.customProfilesSoftCap} custom", + style: ShadTheme.of(context).textTheme.small.copyWith( + color: Settings.tacticalVioletTheme.mutedForeground, + ), + ), + child: const _ThemeProfilesList(), ); } } -// ─── Zone 1: Active Theme (Document State) ──────────────────── - -class _ActiveThemeCard extends ConsumerStatefulWidget { - const _ActiveThemeCard(); - - @override - ConsumerState<_ActiveThemeCard> createState() => _ActiveThemeCardState(); -} - -class _ActiveThemeCardState extends ConsumerState<_ActiveThemeCard> { - String? _profileIdBeforeCustomize; - bool _showSaveForm = false; - late final TextEditingController _saveNameController; - - @override - void initState() { - super.initState(); - _saveNameController = TextEditingController(); - } - - @override - void dispose() { - _saveNameController.dispose(); - super.dispose(); - } +class _ThemeProfilesList extends ConsumerWidget { + const _ThemeProfilesList(); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final profilesState = ref.watch(mapThemeProfilesProvider); final strategyTheme = ref.watch(strategyThemeProvider); - final effectivePalette = ref.watch(effectiveMapThemePaletteProvider); final hasActiveStrategy = ref.watch(strategyProvider).stratName != null; - final profilesState = ref.watch(mapThemeProfilesProvider); - - final isOverride = strategyTheme.overridePalette != null; - final assignedProfileId = strategyTheme.profileId ?? - MapThemeProfilesProvider.immutableDefaultProfileId; - final assignedProfile = profilesState.profiles.firstWhere( - (p) => p.id == assignedProfileId, - orElse: () => MapThemeProfilesProvider.immutableDefaultProfile, - ); + final overridePalette = + hasActiveStrategy ? strategyTheme.overridePalette : null; + final activeProfileId = !hasActiveStrategy || overridePalette != null + ? null + : (strategyTheme.profileId ?? + MapThemeProfilesProvider.immutableDefaultProfileId); + final customCount = + profilesState.profiles.where((p) => !p.isBuiltIn).length; + final canCreate = + customCount < MapThemeProfilesProvider.customProfilesSoftCap; - return AnimatedSize( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - alignment: Alignment.topCenter, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Current strategy theme", - style: ShadTheme.of(context).textTheme.small.copyWith( - color: Settings.tacticalVioletTheme.mutedForeground, - letterSpacing: 0.3, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + if (overridePalette != null) ...[ + _ProfileListRow( + title: "Custom", + tags: const ["This strategy only"], + palette: overridePalette, + isSelected: true, + onTap: null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: canCreate + ? () => showMapThemeEditorDialog( + context, + mode: MapThemeEditorMode.createProfile, + initialPalette: overridePalette, + ) + : null, + child: const Text("Save as profile"), ), - ), - const SizedBox(height: 10), - if (!hasActiveStrategy) - Text( - "Open or create a strategy to assign a map theme.", - style: ShadTheme.of(context).textTheme.small.copyWith( - color: Settings.tacticalVioletTheme.mutedForeground, + const SizedBox(width: 4), + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () => showMapThemeEditorDialog( + context, + mode: MapThemeEditorMode.customizeStrategy, + initialPalette: overridePalette, ), - ) - else - AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - switchInCurve: Curves.easeOut, - switchOutCurve: Curves.easeIn, - layoutBuilder: (currentChild, previousChildren) { - return Stack( - alignment: Alignment.topCenter, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, - ], - ); - }, - child: isOverride - ? _buildOverrideState(context, effectivePalette) - : _buildProfileAssignedState(context, assignedProfile), + child: const Text("Edit"), + ), + ], ), + ), + const SizedBox(height: 2), ], - ), - ); - } - - Widget _buildProfileAssignedState( - BuildContext context, MapThemeProfile profile) { - return SizedBox( - width: double.infinity, - key: const ValueKey('profile-assigned'), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - profile.name, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - const SizedBox(width: 10), - _PaletteSwatches(palette: profile.palette), - const SizedBox(width: 10), - ShadButton.secondary( - size: ShadButtonSize.sm, - leading: const Icon(Icons.edit_outlined, size: 14), - onPressed: () { - _profileIdBeforeCustomize = - ref.read(strategyThemeProvider).profileId ?? - MapThemeProfilesProvider.immutableDefaultProfileId; - ref - .read(strategyProvider.notifier) - .setThemeOverrideForCurrentStrategy( - ref.read(effectiveMapThemePaletteProvider)); - }, - child: const Text("Customize"), - ), + for (final profile in profilesState.profiles) ...[ + _ProfileListRow( + title: profile.name, + tags: [ + if (profile.id == profilesState.defaultProfileIdForNewStrategies) + "Default", + if (profile.isBuiltIn) "Built-in", ], + palette: profile.palette, + isSelected: activeProfileId == profile.id, + onTap: hasActiveStrategy + ? () => _selectProfile( + context, + ref, + profile: profile, + hasOverride: overridePalette != null, + ) + : null, + trailing: _profileRowTrailing( + context, + ref, + profile: profile, + isActive: activeProfileId == profile.id, + isDefault: + profile.id == profilesState.defaultProfileIdForNewStrategies, + ), ), + const SizedBox(height: 2), ], - ), + _NewProfileRow( + enabled: canCreate, + onTap: () => showMapThemeEditorDialog( + context, + mode: MapThemeEditorMode.createProfile, + initialPalette: ref.read(effectiveMapThemePaletteProvider), + ), + ), + ], ); } - Widget _buildOverrideState(BuildContext context, MapThemePalette palette) { - return SizedBox( - width: double.infinity, - key: const ValueKey('override-active'), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: - Settings.tacticalVioletTheme.primary.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - "STRATEGY OVERRIDE", - style: ShadTheme.of(context).textTheme.small.copyWith( - color: Settings.tacticalVioletTheme.primary, - fontSize: 10, - fontWeight: FontWeight.w600, - letterSpacing: 0.8, - ), - ), - ), - const SizedBox(height: 10), - _PaletteEditor( - label: "", - palette: palette, - onChanged: (nextPalette) { - ref - .read(strategyProvider.notifier) - .setThemeOverrideForCurrentStrategy(nextPalette); - }, - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ShadButton.secondary( - leading: const Icon(LucideIcons.undo), - onPressed: () { - final restoreId = _profileIdBeforeCustomize ?? - MapThemeProfilesProvider.immutableDefaultProfileId; - ref - .read(strategyProvider.notifier) - .setThemeProfileForCurrentStrategy(restoreId); - setState(() { - _profileIdBeforeCustomize = null; - _showSaveForm = false; - }); - }, - child: const Text("Revert to profile"), - ), - const SizedBox(width: 4), - ShadButton( - leading: _showSaveForm - ? const Icon(LucideIcons.x) - : const Icon(LucideIcons.save), - onPressed: () { - setState(() { - _showSaveForm = !_showSaveForm; - if (_showSaveForm) { - final profiles = ref.read(mapThemeProfilesProvider); - _saveNameController.text = - "Profile ${MapThemeProfilesProvider.nextGeneratedProfileNumber( - profiles.profiles.where((p) => !p.isBuiltIn).toList(), - )}"; - } - }); - }, - child: Text(_showSaveForm ? "Cancel" : "Save as Profile"), - ), - ], - ), - if (_showSaveForm) ...[ - const SizedBox(height: 10), - _buildSaveForm(context, palette), - ], - ], - ), - ); + Future _selectProfile( + BuildContext context, + WidgetRef ref, { + required MapThemeProfile profile, + required bool hasOverride, + }) async { + if (hasOverride) { + final confirmed = await ConfirmAlertDialog.show( + context: context, + title: "Discard custom colors?", + content: + "This strategy's custom colors will be replaced with \"${profile.name}\" and can't be brought back.", + confirmText: "Discard", + isDestructive: true, + ); + if (!confirmed || !context.mounted) return; + } + ref + .read(strategyProvider.notifier) + .setThemeProfileForCurrentStrategy(profile.id); } - Widget _buildSaveForm(BuildContext context, MapThemePalette palette) { - return Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Settings.tacticalVioletTheme.background, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Settings.tacticalVioletTheme.border), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Profile Name", - style: ShadTheme.of(context).textTheme.small.copyWith( - color: Settings.tacticalVioletTheme.mutedForeground, - ), + Widget? _profileRowTrailing( + BuildContext context, + WidgetRef ref, { + required MapThemeProfile profile, + required bool isActive, + required bool isDefault, + }) { + final children = [ + if (isActive) + ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () => showMapThemeEditorDialog( + context, + mode: MapThemeEditorMode.customizeStrategy, + initialPalette: ref.read(effectiveMapThemePaletteProvider), ), - const SizedBox(height: 6), - CustomTextField( - controller: _saveNameController, - hintText: "Enter a name", - ), - const SizedBox(height: 8), - Align( - alignment: Alignment.centerRight, - child: ShadButton( - onPressed: () async { - final trimmedName = _saveNameController.text.trim(); - final createdProfile = await ref - .read(mapThemeProfilesProvider.notifier) - .createProfile(name: trimmedName, palette: palette); - if (createdProfile == null) { - Settings.showToast( - message: "Profile limit reached or invalid name.", - backgroundColor: Settings.tacticalVioletTheme.destructive, - ); - return; - } - ref - .read(strategyProvider.notifier) - .setThemeProfileForCurrentStrategy(createdProfile.id); - if (!mounted) return; - Settings.showToast( - message: "Profile saved and applied.", - backgroundColor: Settings.tacticalVioletTheme.primary, - ); - setState(() { - _showSaveForm = false; - _profileIdBeforeCustomize = null; - }); - }, - child: const Text("Save"), - ), - ), - ], - ), + child: const Text("Customize"), + ), + if (!(profile.isBuiltIn && isDefault)) + _ProfileContextMenuButton(profile: profile, isDefault: isDefault), + ]; + if (children.isEmpty) return null; + if (children.length == 1) return children.single; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + children.first, + const SizedBox(width: 4), + ...children.skip(1), + ], ); } } -// ─── Zone 2: Profile Library (Global Preferences) ───────────── - -class _ProfileLibrarySection extends ConsumerStatefulWidget { - const _ProfileLibrarySection({ - required this.scope, - this.onManageProfiles, - }); - - final MapThemeSettingsScope scope; - final VoidCallback? onManageProfiles; +class _NewProfileRow extends StatelessWidget { + const _NewProfileRow({required this.enabled, required this.onTap}); - @override - ConsumerState<_ProfileLibrarySection> createState() => - _ProfileLibrarySectionState(); -} + final bool enabled; + final VoidCallback onTap; -class _ProfileLibrarySectionState - extends ConsumerState<_ProfileLibrarySection> { @override Widget build(BuildContext context) { - final profilesState = ref.watch(mapThemeProfilesProvider); - final strategyTheme = ref.watch(strategyThemeProvider); - final hasActiveStrategy = ref.watch(strategyProvider).stratName != null; - final isStrategyScope = widget.scope == MapThemeSettingsScope.strategy; - - final activeProfileId = strategyTheme.overridePalette == null - ? (strategyTheme.profileId ?? - MapThemeProfilesProvider.immutableDefaultProfileId) - : null; - - final customCount = - profilesState.profiles.where((p) => !p.isBuiltIn).length; + const theme = Settings.tacticalVioletTheme; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - isStrategyScope ? "Choose a profile" : "Saved profiles", - style: ShadTheme.of(context).textTheme.small.copyWith( - color: Settings.tacticalVioletTheme.mutedForeground, - letterSpacing: 0.3, - ), - ), - Text( - "$customCount/${MapThemeProfilesProvider.customProfilesSoftCap} custom", - style: ShadTheme.of(context).textTheme.small.copyWith( - color: Settings.tacticalVioletTheme.mutedForeground, + return Material( + type: MaterialType.transparency, + child: InkWell( + onTap: enabled ? onTap : null, + mouseCursor: + enabled ? SystemMouseCursors.click : SystemMouseCursors.basic, + borderRadius: BorderRadius.circular(8), + hoverColor: theme.secondary.withValues(alpha: 0.45), + splashFactory: NoSplash.splashFactory, + child: Opacity( + opacity: enabled ? 1 : 0.5, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: Row( + children: [ + SizedBox( + width: 22, + child: Icon( + LucideIcons.plus, + size: 15, + color: theme.mutedForeground, ), - ), - ], - ), - const SizedBox(height: 8), - if (isStrategyScope && widget.onManageProfiles != null) ...[ - Align( - alignment: Alignment.centerLeft, - child: ShadButton.ghost( - size: ShadButtonSize.sm, - leading: const Icon(LucideIcons.arrowRight), - onPressed: widget.onManageProfiles!, - child: const Text("Manage profiles in App settings"), + ), + Text( + "New profile", + style: ShadTheme.of(context).textTheme.small.copyWith( + color: theme.mutedForeground, + ), + ), + ], ), ), - const SizedBox(height: 8), - ], - for (final profile in profilesState.profiles) ...[ - _ProfileListRow( - title: profile.name, - palette: profile.palette, - isSelected: isStrategyScope && activeProfileId == profile.id, - isDefault: !isStrategyScope && - profile.id == profilesState.defaultProfileIdForNewStrategies, - onTap: isStrategyScope && hasActiveStrategy - ? () { - ref - .read(strategyProvider.notifier) - .setThemeProfileForCurrentStrategy(profile.id); - } - : null, - trailing: isStrategyScope || - (profile.isBuiltIn && - profile.id == - profilesState.defaultProfileIdForNewStrategies) - ? null - : _ProfileContextMenuButton( - profile: profile, - isDefault: profile.id == - profilesState.defaultProfileIdForNewStrategies, - ), - ), - const SizedBox(height: 6), - ], - ], + ), + ), ); } } @@ -532,27 +329,23 @@ class _ProfileContextMenuButtonState ); if (newName == null || newName.isEmpty) return; - await ref + final renamed = await ref .read(mapThemeProfilesProvider.notifier) .renameProfile(profileId: widget.profile.id, newName: newName); + if (!renamed) { + Settings.showToast( + message: "Couldn't rename this profile.", + backgroundColor: Settings.tacticalVioletTheme.destructive, + ); + } } Future _editProfilePalette() async { - final nextPalette = await _showProfilePaletteDialog( - context: context, + await showMapThemeEditorDialog( + context, + mode: MapThemeEditorMode.editProfile, profile: widget.profile, - ); - if (nextPalette == null) return; - - await ref.read(mapThemeProfilesProvider.notifier).updateProfilePalette( - profileId: widget.profile.id, - palette: nextPalette, - ); - if (!mounted) return; - - Settings.showToast( - message: "Profile colors updated.", - backgroundColor: Settings.tacticalVioletTheme.primary, + initialPalette: widget.profile.palette, ); } @@ -588,82 +381,85 @@ class _ProfileListRow extends StatelessWidget { required this.title, required this.palette, required this.isSelected, - required this.isDefault, required this.onTap, + this.tags = const [], this.trailing, }); final String title; final MapThemePalette palette; final bool isSelected; - final bool isDefault; + final List tags; final VoidCallback? onTap; final Widget? trailing; @override Widget build(BuildContext context) { - return InkWell( - mouseCursor: - onTap == null ? SystemMouseCursors.basic : SystemMouseCursors.click, - borderRadius: BorderRadius.circular(10), - onTap: onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( + const theme = Settings.tacticalVioletTheme; + + return Material( + type: MaterialType.transparency, + child: InkWell( + onTap: onTap, + mouseCursor: + onTap == null ? SystemMouseCursors.basic : SystemMouseCursors.click, + borderRadius: BorderRadius.circular(8), + hoverColor: theme.secondary.withValues(alpha: 0.45), + highlightColor: theme.secondary.withValues(alpha: 0.6), + splashFactory: NoSplash.splashFactory, + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), color: isSelected - ? Settings.tacticalVioletTheme.primary.withValues(alpha: 0.5) - : Settings.tacticalVioletTheme.border, - width: 1, + ? theme.secondary.withValues(alpha: 0.9) + : Colors.transparent, ), - color: isSelected - ? Settings.tacticalVioletTheme.primary.withValues(alpha: 0.08) - : Settings.tacticalVioletTheme.secondary.withValues(alpha: 0.28), - ), - child: Row( - children: [ - Expanded( - child: Row( - children: [ - Flexible( - child: Text( - title, - overflow: TextOverflow.ellipsis, - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: Row( + children: [ + if (onTap != null || isSelected) + SizedBox( + width: 22, + child: isSelected + ? Icon( + LucideIcons.check, + size: 15, + color: theme.primary, + ) + : null, ), - if (isDefault) ...[ - const SizedBox(width: 6), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, vertical: 1), - decoration: BoxDecoration( - color: Settings.tacticalVioletTheme.primary - .withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(3), + Expanded( + child: Row( + children: [ + Flexible( + child: Text( + title, + overflow: TextOverflow.ellipsis, + ), ), - child: Text( - "DEFAULT", - style: ShadTheme.of(context).textTheme.small.copyWith( - color: Settings.tacticalVioletTheme.primary, - fontSize: 9, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - ), - ), - ), - ], + if (tags.isNotEmpty) ...[ + const SizedBox(width: 8), + Text( + tags.join(' · '), + style: ShadTheme.of(context).textTheme.small.copyWith( + color: theme.mutedForeground, + fontSize: 12, + ), + ), + ], + ], + ), + ), + const SizedBox(width: 8), + _PaletteSwatches(palette: palette), + if (trailing != null) ...[ + const SizedBox(width: 4), + trailing!, ], - ), + ], ), - const SizedBox(width: 8), - _PaletteSwatches(palette: palette), - if (trailing != null) ...[ - const SizedBox(width: 4), - trailing!, - ], - ], + ), ), ), ); @@ -672,64 +468,6 @@ class _ProfileListRow extends StatelessWidget { // ─── Palette Widgets ────────────────────────────────────────── -class _PaletteEditor extends StatelessWidget { - const _PaletteEditor({ - required this.label, - required this.palette, - required this.onChanged, - }); - - final String label; - final MapThemePalette palette; - final ValueChanged onChanged; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (label.isNotEmpty) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label), - ], - ), - const SizedBox(height: 6), - ], - Row( - children: [ - _EditableSwatch( - label: "Base", - color: palette.baseColor, - onPick: (color) { - onChanged(palette.copyWith(baseColorValue: color.toARGB32())); - }, - ), - const SizedBox(width: 8), - _EditableSwatch( - label: "Detail", - color: palette.detailColor, - onPick: (color) { - onChanged(palette.copyWith(detailColorValue: color.toARGB32())); - }, - ), - const SizedBox(width: 8), - _EditableSwatch( - label: "Highlight", - color: palette.highlightColor, - onPick: (color) { - onChanged( - palette.copyWith(highlightColorValue: color.toARGB32())); - }, - ), - ], - ), - ], - ); - } -} - class _PaletteSwatches extends StatelessWidget { const _PaletteSwatches({required this.palette}); @@ -749,55 +487,6 @@ class _PaletteSwatches extends StatelessWidget { } } -class _EditableSwatch extends StatelessWidget { - const _EditableSwatch({ - required this.label, - required this.color, - required this.onPick, - }); - - final String label; - final Color color; - final ValueChanged onPick; - - @override - Widget build(BuildContext context) { - return Expanded( - child: InkWell( - mouseCursor: SystemMouseCursors.click, - borderRadius: BorderRadius.circular(8), - onTap: () async { - final picked = await _showColorPickerDialog( - context: context, - initialColor: color, - title: "Pick $label color", - ); - if (picked != null) { - onPick(picked); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: ShadTheme.of(context).textTheme.small), - const SizedBox(height: 4), - Container( - height: 42, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Settings.tacticalVioletTheme.border, - ), - color: color, - ), - ), - ], - ), - ), - ); - } -} - class _Swatch extends StatelessWidget { const _Swatch({required this.color}); @@ -819,105 +508,6 @@ class _Swatch extends StatelessWidget { // ─── Dialogs ────────────────────────────────────────────────── -Future _showColorPickerDialog({ - required BuildContext context, - required Color initialColor, - required String title, -}) async { - var workingColor = initialColor; - return showShadDialog( - context: context, - builder: (context) { - return StatefulBuilder( - builder: (context, setState) { - return ShadDialog( - title: Text(title), - actions: [ - ShadButton.secondary( - onPressed: () => Navigator.of(context).pop(), - child: const Text("Cancel"), - ), - ShadButton( - onPressed: () => Navigator.of(context).pop(workingColor), - child: const Text("Apply"), - ), - ], - child: Material( - color: Colors.transparent, - child: SizedBox( - width: 320, - child: BetterColorPicker( - value: workingColor, - initialMode: BetterColorPickerMode.hsv, - style: icarusColorPickerStyle, - onChanging: (color) { - setState(() { - workingColor = color; - }); - }, - onChanged: (color) { - setState(() { - workingColor = color; - }); - }, - ), - ), - ), - ); - }, - ); - }, - ); -} - -Future _showProfilePaletteDialog({ - required BuildContext context, - required MapThemeProfile profile, -}) async { - var workingPalette = profile.palette; - return showShadDialog( - context: context, - builder: (dialogContext) { - return StatefulBuilder( - builder: (context, setState) { - return ShadDialog( - title: Text("Edit ${profile.name}"), - description: const Text( - "Changes apply anywhere this profile is used.", - ), - actions: [ - ShadButton.secondary( - onPressed: () => Navigator.of(dialogContext).pop(), - child: const Text("Cancel"), - ), - ShadButton( - onPressed: () => - Navigator.of(dialogContext).pop(workingPalette), - child: const Text("Save colors"), - ), - ], - child: Material( - color: Colors.transparent, - child: SizedBox( - width: 360, - child: _PaletteEditor( - label: "", - palette: workingPalette, - onChanged: (nextPalette) { - setState(() { - workingPalette = nextPalette; - }); - }, - ), - ), - ), - ); - }, - ); - }, - ); -} - Future _showRenameDialog({ required BuildContext context, required String currentName, diff --git a/lib/widgets/settings_scope_card.dart b/lib/widgets/settings_scope_card.dart index 26dea21e..5ba9860c 100644 --- a/lib/widgets/settings_scope_card.dart +++ b/lib/widgets/settings_scope_card.dart @@ -6,13 +6,13 @@ class SettingsScopeCard extends StatelessWidget { const SettingsScopeCard({ super.key, required this.title, - required this.description, + this.description, required this.child, this.trailing, }); final String title; - final String description; + final String? description; final Widget child; final Widget? trailing; @@ -26,10 +26,12 @@ class SettingsScopeCard extends StatelessWidget { Expanded( child: Text( title, - style: ShadTheme.of(context) - .textTheme - .lead - .copyWith(fontWeight: FontWeight.w700), + style: ShadTheme.of(context).textTheme.p.copyWith( + color: Settings.tacticalVioletTheme.foreground, + fontSize: 17, + fontWeight: FontWeight.w500, + height: 1.25, + ), ), ), if (trailing != null) ...[ @@ -38,15 +40,16 @@ class SettingsScopeCard extends StatelessWidget { ], ], ), - const SizedBox(height: 4), - Text( - description, - style: ShadTheme.of(context).textTheme.small.copyWith( - color: Settings.tacticalVioletTheme.mutedForeground, - height: 1.35, - ), - ), - const SizedBox(height: 8), + if (description != null) ...[ + const SizedBox(height: 4), + Text( + description!, + style: ShadTheme.of(context).textTheme.small.copyWith( + color: Settings.tacticalVioletTheme.mutedForeground, + height: 1.35, + ), + ), + ], child, ], ); diff --git a/lib/widgets/settings_tab.dart b/lib/widgets/settings_tab.dart index 7ee83a88..71c4a7fb 100644 --- a/lib/widgets/settings_tab.dart +++ b/lib/widgets/settings_tab.dart @@ -29,7 +29,6 @@ enum _SettingsSection { globalDefaults, globalSaving, globalMapVisibility, - globalMapProfiles, globalPrivacy, shortcuts, } @@ -37,9 +36,9 @@ enum _SettingsSection { class SettingsTab extends ConsumerStatefulWidget { const SettingsTab({super.key}); - static const double _dialogWidth = 860; - static const double _dialogHeight = 640; - static const double _navigationWidth = 196; + static const double _maxDialogWidth = 1080; + static const double _maxDialogHeight = 820; + static const double _navigationWidth = 208; @override ConsumerState createState() => _SettingsTabState(); @@ -61,25 +60,20 @@ class _SettingsTabState extends ConsumerState { @override Widget build(BuildContext context) { - final activeStrategyName = ref.watch(strategyProvider).stratName; final strategySettings = ref.watch(strategySettingsProvider); final mapState = ref.watch(mapProvider); final appPreferences = ref.watch(appPreferencesProvider); - final scopeLabel = switch (_mode) { - _SettingsMode.strategy => 'Current strategy', - _SettingsMode.global => 'App-wide', - _SettingsMode.shortcuts => 'App-wide', - }; - final scopeValue = switch (_mode) { - _SettingsMode.strategy => activeStrategyName, - _SettingsMode.global => 'Defaults', - _SettingsMode.shortcuts => 'Keybinds', - }; + + final screenSize = MediaQuery.sizeOf(context); + final dialogWidth = + (screenSize.width - 96).clamp(560.0, SettingsTab._maxDialogWidth); + final dialogHeight = + (screenSize.height - 80).clamp(420.0, SettingsTab._maxDialogHeight); return ShadDialog( - constraints: const BoxConstraints( - maxWidth: SettingsTab._dialogWidth, - maxHeight: SettingsTab._dialogHeight, + constraints: BoxConstraints( + maxWidth: dialogWidth, + maxHeight: dialogHeight, ), padding: EdgeInsets.zero, scrollable: false, @@ -87,13 +81,12 @@ class _SettingsTabState extends ConsumerState { child: Material( color: Colors.transparent, child: SizedBox( - width: SettingsTab._dialogWidth, - height: SettingsTab._dialogHeight, + width: dialogWidth, + height: dialogHeight, child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _SettingsNavigationRail( - mode: _mode, selectedSection: _selectedSection, onSectionSelected: _selectSection, ), @@ -104,56 +97,29 @@ class _SettingsTabState extends ConsumerState { ), Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(24, 22, 24, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(right: 36), - child: _SettingsScopeHeader( - label: scopeLabel, - value: scopeValue, - ), - ), - const SizedBox(height: 14), - Expanded( - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith(scrollbars: false), - child: SingleChildScrollView( - controller: _scrollController, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 180), - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInCubic, - child: switch (_mode) { - _SettingsMode.strategy => - _StrategySettingsSections( - key: const ValueKey('strategy-settings'), - sectionKeys: _sectionKeys, - activeStrategyName: activeStrategyName, - strategySettings: strategySettings, - onManageThemeProfiles: () => _selectSection( - _SettingsSection.globalMapProfiles, - ), - ), - _SettingsMode.global => _GlobalSettingsSections( - key: const ValueKey('global-settings'), - sectionKeys: _sectionKeys, - appPreferences: appPreferences, - mapState: mapState, - ), - _SettingsMode.shortcuts => - _ShortcutSettingsSection( - key: _sectionKeys[ - _SettingsSection.shortcuts], - ), - }, - ), + padding: const EdgeInsets.fromLTRB(24, 24, 24, 0), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: _scrollController, + child: switch (_mode) { + _SettingsMode.strategy => _StrategySettingsSections( + key: const ValueKey('strategy-settings'), + sectionKeys: _sectionKeys, + strategySettings: strategySettings, ), - ), - ), - ], + _SettingsMode.global => _GlobalSettingsSections( + key: const ValueKey('global-settings'), + sectionKeys: _sectionKeys, + appPreferences: appPreferences, + mapState: mapState, + ), + _SettingsMode.shortcuts => _ShortcutSettingsSection( + key: _sectionKeys[_SettingsSection.shortcuts], + ), + }, + ), ), ), ), @@ -166,6 +132,7 @@ class _SettingsTabState extends ConsumerState { void _selectSection(_SettingsSection section) { final nextMode = _modeForSection(section); + final modeChanged = nextMode != _mode; setState(() { _mode = nextMode; _selectedSection = section; @@ -174,12 +141,18 @@ class _SettingsTabState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback((_) { final context = _sectionKeys[section]?.currentContext; if (context == null || !mounted) return; - Scrollable.ensureVisible( - context, - duration: const Duration(milliseconds: 220), - curve: Curves.easeOutCubic, - alignment: 0, - ); + if (modeChanged) { + // New pane: land on the section directly, no scroll animation from a + // stale offset. + Scrollable.ensureVisible(context, alignment: 0); + } else { + Scrollable.ensureVisible( + context, + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + alignment: 0, + ); + } }); } @@ -191,7 +164,6 @@ class _SettingsTabState extends ConsumerState { case _SettingsSection.globalDefaults: case _SettingsSection.globalSaving: case _SettingsSection.globalMapVisibility: - case _SettingsSection.globalMapProfiles: case _SettingsSection.globalPrivacy: return _SettingsMode.global; case _SettingsSection.shortcuts: @@ -200,72 +172,15 @@ class _SettingsTabState extends ConsumerState { } } -class _SettingsScopeHeader extends StatelessWidget { - const _SettingsScopeHeader({ - required this.label, - required this.value, - }); - - final String label; - final String? value; - - @override - Widget build(BuildContext context) { - final theme = ShadTheme.of(context); - final displayValue = value ?? 'Open strategy'; - - return Row( - children: [ - Text( - label.toUpperCase(), - style: theme.textTheme.small.copyWith( - color: Settings.tacticalVioletTheme.mutedForeground, - fontWeight: FontWeight.w800, - letterSpacing: 0.45, - ), - ), - const SizedBox(width: 10), - Flexible( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5), - decoration: BoxDecoration( - color: Settings.tacticalVioletTheme.secondary - .withValues(alpha: 0.72), - borderRadius: BorderRadius.circular(999), - border: Border.all( - color: - Settings.tacticalVioletTheme.border.withValues(alpha: 0.9), - ), - ), - child: Text( - displayValue, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.small.copyWith( - color: Settings.tacticalVioletTheme.foreground, - fontWeight: FontWeight.w700, - height: 1.1, - ), - ), - ), - ), - ], - ); - } -} - class _StrategySettingsSections extends ConsumerWidget { const _StrategySettingsSections({ super.key, required this.sectionKeys, - required this.activeStrategyName, required this.strategySettings, - required this.onManageThemeProfiles, }); final Map<_SettingsSection, GlobalKey> sectionKeys; - final String? activeStrategyName; final StrategySettings strategySettings; - final VoidCallback onManageThemeProfiles; @override Widget build(BuildContext context, WidgetRef ref) { @@ -274,14 +189,12 @@ class _StrategySettingsSections extends ConsumerWidget { children: [ SettingsScopeCard( key: sectionKeys[_SettingsSection.strategyObjects], - title: "Strategy object styling", - description: activeStrategyName == null - ? "Resize markers and control how placed objects render." - : "Changes apply to \"$activeStrategyName\".", + title: "Object styling", child: Column( children: [ _SettingsSliderTile( icon: Icons.person_pin_circle_outlined, + iconColor: Settings.settingsAgentAccent, title: "Agent markers", description: "Resize placed agents and view tools for this strategy.", @@ -307,9 +220,9 @@ class _StrategySettingsSections extends ConsumerWidget { ); }, ), - const _SettingsItemDivider(), _SettingsSliderTile( icon: Icons.auto_awesome_outlined, + iconColor: Settings.settingsAbilityAccent, title: "Ability markers", description: "Resize utility icons and placement helpers for this strategy.", @@ -335,9 +248,9 @@ class _StrategySettingsSections extends ConsumerWidget { ); }, ), - const _SettingsItemDivider(), _SettingsToggleTile( icon: Icons.contrast_outlined, + iconColor: Settings.settingsNeutralAccent, title: "Neutral team marker colors", description: "Render ally and enemy marker accents as matching-brightness greys.", @@ -358,15 +271,10 @@ class _StrategySettingsSections extends ConsumerWidget { ], ), ), - const SizedBox(height: 20), - const _SectionDivider(), - const SizedBox(height: 20), + const SizedBox(height: 28), KeyedSubtree( key: sectionKeys[_SettingsSection.strategyMapTheme], - child: MapThemeSettingsSection( - scope: MapThemeSettingsScope.strategy, - onManageProfiles: onManageThemeProfiles, - ), + child: const MapThemeSettingsSection(), ), const SizedBox(height: 24), ], @@ -394,11 +302,11 @@ class _GlobalSettingsSections extends ConsumerWidget { SettingsScopeCard( key: sectionKeys[_SettingsSection.globalDefaults], title: "New strategy defaults", - description: "Set the marker styling each new strategy starts with.", child: Column( children: [ _SettingsSliderTile( icon: Icons.person_pin_circle_outlined, + iconColor: Settings.settingsAgentAccent, title: "Default agent markers", description: "Default size for agent markers in new strategies.", @@ -413,9 +321,9 @@ class _GlobalSettingsSections extends ConsumerWidget { .setDefaultAgentSizeForNewStrategies(value); }, ), - const _SettingsItemDivider(), _SettingsSliderTile( icon: Icons.auto_awesome_outlined, + iconColor: Settings.settingsAbilityAccent, title: "Default ability markers", description: "Default size for ability markers in new strategies.", @@ -430,9 +338,9 @@ class _GlobalSettingsSections extends ConsumerWidget { .setDefaultAbilitySizeForNewStrategies(value); }, ), - const _SettingsItemDivider(), _SettingsToggleTile( icon: Icons.contrast_outlined, + iconColor: Settings.settingsNeutralAccent, title: "Neutral marker colors by default", description: "Use grey ally and enemy accents for new strategies.", @@ -449,18 +357,15 @@ class _GlobalSettingsSections extends ConsumerWidget { ], ), ), - const SizedBox(height: 20), - const _SectionDivider(), - const SizedBox(height: 20), + const SizedBox(height: 28), SettingsScopeCard( key: sectionKeys[_SettingsSection.globalSaving], title: "Workspace behavior", - description: - "Control how Icarus persists strategy edits while you work.", child: Column( children: [ _SettingsToggleTile( icon: Icons.save_outlined, + iconColor: Settings.settingsPersistenceAccent, title: "Autosave", description: "Automatically save the current strategy after 15 seconds of inactivity. When off, Icarus will ask before you leave unsaved work.", @@ -474,9 +379,9 @@ class _GlobalSettingsSections extends ConsumerWidget { .refreshAutosaveScheduling(); }, ), - const _SettingsItemDivider(), _SettingsToggleTile( icon: Icons.sports_esports_outlined, + iconColor: Settings.settingsDiscordAccent, title: "Discord Rich Presence", description: "Show the current map and attack or defense side on your Discord profile. Strategy names are never shared.", @@ -490,17 +395,15 @@ class _GlobalSettingsSections extends ConsumerWidget { ], ), ), - const SizedBox(height: 20), - const _SectionDivider(), - const SizedBox(height: 20), + const SizedBox(height: 28), SettingsScopeCard( key: sectionKeys[_SettingsSection.globalMapVisibility], title: "Workspace map visibility", - description: "Show or hide map reference layers while you work.", child: Column( children: [ _SettingsToggleTile( icon: Icons.grid_on_rounded, + iconColor: Settings.settingsMapAccent, title: "Spawn barriers", description: "Keep round-start barrier guides visible on the map.", @@ -509,9 +412,9 @@ class _GlobalSettingsSections extends ConsumerWidget { ref.read(mapProvider.notifier).updateSpawnBarrier(value); }, ), - const _SettingsItemDivider(), _SettingsToggleTile( icon: Icons.location_on_outlined, + iconColor: Settings.settingsMapAccent, title: "Region names", description: "Show map callout names directly on the canvas.", value: mapState.showRegionNames, @@ -519,9 +422,9 @@ class _GlobalSettingsSections extends ConsumerWidget { ref.read(mapProvider.notifier).updateRegionNames(value); }, ), - const _SettingsItemDivider(), _SettingsToggleTile( icon: Icons.radio_button_checked_outlined, + iconColor: Settings.settingsMapAccent, title: "Ultimate orbs", description: "Display orb pickup markers on supported maps.", value: mapState.showUltOrbs, @@ -532,19 +435,16 @@ class _GlobalSettingsSections extends ConsumerWidget { ], ), ), - const SizedBox(height: 20), - const _SectionDivider(), - const SizedBox(height: 20), + const SizedBox(height: 28), SettingsScopeCard( key: sectionKeys[_SettingsSection.globalPrivacy], title: "Privacy", - description: - "Control anonymous, privacy-preserving product analytics.", child: _SettingsToggleTile( icon: Icons.analytics_outlined, + iconColor: Settings.settingsPersistenceAccent, title: "Anonymous analytics", description: - "Share app opens and a few feature-use events. Icarus never sends strategy names or content, personal details, screen recordings, or crash reports.", + "Share app opens and a few feature-use events — never strategy content or personal details.", value: ref.watch(analyticsEnabledProvider), onChanged: (value) async { await ref @@ -553,15 +453,6 @@ class _GlobalSettingsSections extends ConsumerWidget { }, ), ), - const SizedBox(height: 20), - const _SectionDivider(), - const SizedBox(height: 20), - KeyedSubtree( - key: sectionKeys[_SettingsSection.globalMapProfiles], - child: const MapThemeSettingsSection( - scope: MapThemeSettingsScope.global, - ), - ), const SizedBox(height: 24), ], ); @@ -606,11 +497,10 @@ class _ShortcutSettingsSectionState return SettingsScopeCard( title: "Keybinds", - description: - "Edit app-wide keybinds. Search accepts action names or keys.", child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 10), Row( children: [ Expanded( @@ -785,8 +675,7 @@ class _ShortcutTableHeader extends StatelessWidget { "Action", style: theme.textTheme.small.copyWith( color: Settings.tacticalVioletTheme.mutedForeground, - fontWeight: FontWeight.w800, - letterSpacing: 0.35, + fontSize: 12, ), ), ), @@ -798,8 +687,7 @@ class _ShortcutTableHeader extends StatelessWidget { "Binding", style: theme.textTheme.small.copyWith( color: Settings.tacticalVioletTheme.mutedForeground, - fontWeight: FontWeight.w800, - letterSpacing: 0.35, + fontSize: 12, ), ), ), @@ -856,7 +744,7 @@ class _ShortcutBindingRow extends StatelessWidget { definition.title, style: theme.textTheme.p.copyWith( color: Settings.tacticalVioletTheme.foreground, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w400, ), ), ), @@ -877,6 +765,7 @@ class _ShortcutBindingRow extends StatelessWidget { alignment: Alignment.centerRight, child: InkWell( borderRadius: BorderRadius.circular(8), + mouseCursor: SystemMouseCursors.click, onTap: onEdit, child: _ShortcutBindingPill( label: binding.displayLabel(), @@ -939,7 +828,6 @@ class _ShortcutBindingPill extends StatelessWidget { overflow: TextOverflow.ellipsis, style: ShadTheme.of(context).textTheme.small.copyWith( color: Settings.tacticalVioletTheme.foreground, - fontWeight: FontWeight.w800, height: 1.1, ), ), @@ -1065,7 +953,6 @@ class _ShortcutCaptureFieldState extends State<_ShortcutCaptureField> "Press new shortcut...", style: ShadTheme.of(context).textTheme.small.copyWith( color: Settings.tacticalVioletTheme.foreground, - fontWeight: FontWeight.w700, ), ), ), @@ -1088,7 +975,6 @@ class _ShortcutCaptureFieldState extends State<_ShortcutCaptureField> widget.duplicateMessage!, style: ShadTheme.of(context).textTheme.small.copyWith( color: Settings.tacticalVioletTheme.destructive, - fontWeight: FontWeight.w700, ), ), ) @@ -1120,7 +1006,6 @@ class _ShortcutEmptySearch extends StatelessWidget { "No shortcuts match that search.", style: ShadTheme.of(context).textTheme.small.copyWith( color: Settings.tacticalVioletTheme.mutedForeground, - fontWeight: FontWeight.w600, ), ), ), @@ -1130,12 +1015,10 @@ class _ShortcutEmptySearch extends StatelessWidget { class _SettingsNavigationRail extends StatelessWidget { const _SettingsNavigationRail({ - required this.mode, required this.selectedSection, required this.onSectionSelected, }); - final _SettingsMode mode; final _SettingsSection selectedSection; final ValueChanged<_SettingsSection> onSectionSelected; @@ -1152,15 +1035,11 @@ class _SettingsNavigationRail extends StatelessWidget { "Settings", style: ShadTheme.of(context).textTheme.h4.copyWith( color: Settings.tacticalVioletTheme.foreground, - fontWeight: FontWeight.w700, + fontWeight: FontWeight.w500, ), ), - const SizedBox(height: 18), - _SettingsNavHeader( - label: "Current strategy", - isActive: mode == _SettingsMode.strategy, - onTap: () => onSectionSelected(_SettingsSection.strategyObjects), - ), + const SizedBox(height: 20), + const _SettingsNavHeader(label: "Current strategy"), const SizedBox(height: 4), _SettingsNavItem( icon: Icons.tune_outlined, @@ -1174,12 +1053,8 @@ class _SettingsNavigationRail extends StatelessWidget { isSelected: selectedSection == _SettingsSection.strategyMapTheme, onTap: () => onSectionSelected(_SettingsSection.strategyMapTheme), ), - const SizedBox(height: 16), - _SettingsNavHeader( - label: "App-wide", - isActive: mode == _SettingsMode.global, - onTap: () => onSectionSelected(_SettingsSection.globalDefaults), - ), + const SizedBox(height: 20), + const _SettingsNavHeader(label: "App-wide"), const SizedBox(height: 4), _SettingsNavItem( icon: Icons.auto_fix_high_outlined, @@ -1200,12 +1075,6 @@ class _SettingsNavigationRail extends StatelessWidget { onTap: () => onSectionSelected(_SettingsSection.globalMapVisibility), ), - _SettingsNavItem( - icon: Icons.format_color_fill_outlined, - label: "Theme profiles", - isSelected: selectedSection == _SettingsSection.globalMapProfiles, - onTap: () => onSectionSelected(_SettingsSection.globalMapProfiles), - ), _SettingsNavItem( icon: Icons.privacy_tip_outlined, label: "Privacy", @@ -1225,53 +1094,21 @@ class _SettingsNavigationRail extends StatelessWidget { } class _SettingsNavHeader extends StatelessWidget { - const _SettingsNavHeader({ - required this.label, - required this.isActive, - required this.onTap, - }); + const _SettingsNavHeader({required this.label}); final String label; - final bool isActive; - final VoidCallback onTap; @override Widget build(BuildContext context) { - final color = isActive - ? Settings.tacticalVioletTheme.foreground - : Settings.tacticalVioletTheme.mutedForeground; - - return InkWell( - borderRadius: BorderRadius.circular(8), - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 7), - child: Row( - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 160), - curve: Curves.easeOutCubic, - width: 6, - height: 6, - decoration: BoxDecoration( - color: isActive - ? Settings.tacticalVioletTheme.primary - : Settings.tacticalVioletTheme.mutedForeground - .withValues(alpha: 0.36), - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 8), - Text( - label, - style: ShadTheme.of(context).textTheme.small.copyWith( - color: color, - fontWeight: isActive ? FontWeight.w800 : FontWeight.w700, - letterSpacing: 0.25, - ), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Text( + label, + style: ShadTheme.of(context).textTheme.small.copyWith( + color: Settings.tacticalVioletTheme.mutedForeground, + fontSize: 12, + fontWeight: FontWeight.w400, ), - ], - ), ), ); } @@ -1292,51 +1129,53 @@ class _SettingsNavItem extends StatelessWidget { @override Widget build(BuildContext context) { + const theme = Settings.tacticalVioletTheme; + final contentColor = isSelected ? theme.foreground : theme.mutedForeground; + return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 160), - curve: Curves.easeOutCubic, - height: 38, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: isSelected - ? Settings.tacticalVioletTheme.primary.withValues(alpha: 0.14) - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all( + padding: const EdgeInsets.only(bottom: 2), + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: onTap, + mouseCursor: SystemMouseCursors.click, + borderRadius: BorderRadius.circular(8), + hoverColor: theme.secondary.withValues(alpha: 0.45), + highlightColor: theme.secondary.withValues(alpha: 0.6), + splashFactory: NoSplash.splashFactory, + child: Ink( + decoration: BoxDecoration( color: isSelected - ? Settings.tacticalVioletTheme.primary.withValues(alpha: 0.24) + ? theme.secondary.withValues(alpha: 0.9) : Colors.transparent, + borderRadius: BorderRadius.circular(8), ), - ), - child: Row( - children: [ - Icon( - icon, - size: 16, - color: isSelected - ? Settings.tacticalVioletTheme.foreground - : Settings.tacticalVioletTheme.mutedForeground, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - label, - overflow: TextOverflow.ellipsis, - style: ShadTheme.of(context).textTheme.small.copyWith( - color: isSelected - ? Settings.tacticalVioletTheme.foreground - : Settings.tacticalVioletTheme.mutedForeground, - fontWeight: - isSelected ? FontWeight.w700 : FontWeight.w500, + child: SizedBox( + height: 34, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + Icon( + icon, + size: 16, + color: isSelected ? theme.primary : contentColor, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + label, + overflow: TextOverflow.ellipsis, + style: ShadTheme.of(context).textTheme.small.copyWith( + color: contentColor, + fontWeight: FontWeight.w400, + ), ), + ), + ], ), ), - ], + ), ), ), ), @@ -1347,6 +1186,7 @@ class _SettingsNavItem extends StatelessWidget { class _SettingsSliderTile extends StatefulWidget { const _SettingsSliderTile({ required this.icon, + required this.iconColor, required this.title, required this.description, required this.value, @@ -1359,6 +1199,7 @@ class _SettingsSliderTile extends StatefulWidget { }); final IconData icon; + final Color iconColor; final String title; final String description; final double value; @@ -1379,67 +1220,78 @@ class _SettingsSliderTileState extends State<_SettingsSliderTile> { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.only(top: 10, bottom: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - _SettingLeadingIcon( - icon: widget.icon, - accentColor: widget.accentColor, + SizedBox( + width: 24, + height: 24, + child: Icon( + widget.icon, + size: 20, + color: widget.iconColor, + ), ), - const SizedBox(width: 10), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.title, - style: const TextStyle(fontWeight: FontWeight.w600), - ), + Text(widget.title), const SizedBox(height: 2), Text( widget.description, style: ShadTheme.of(context).textTheme.small.copyWith( color: Settings.tacticalVioletTheme.mutedForeground, - height: 1.3, + fontSize: 12, + height: 1.35, ), ), ], ), ), - const SizedBox(width: 8), - _SettingValuePill( - value: widget.value.toStringAsFixed(0), - accentColor: widget.accentColor, + const SizedBox(width: 12), + Text( + widget.value.toStringAsFixed(0), + style: ShadTheme.of(context).textTheme.small.copyWith( + color: Settings.tacticalVioletTheme.foreground, + fontFeatures: const [FontFeature.tabularFigures()], + ), ), ], ), - const SizedBox(height: 8), - SliderTheme( - data: SliderTheme.of(context).copyWith( - activeTrackColor: widget.accentColor, - thumbColor: widget.accentColor, - overlayColor: widget.accentColor.withValues(alpha: 0.12), - inactiveTrackColor: Settings.tacticalVioletTheme.secondary, - trackHeight: 2.8, - ), - child: Slider( - min: widget.min, - max: widget.max, - divisions: widget.divisions, - value: widget.value, - onChangeStart: (value) => _dragStartValue = value, - onChangeEnd: (value) { - widget.onChangeCommitted?.call( - _dragStartValue ?? widget.value, - value, - ); - _dragStartValue = null; - }, - onChanged: widget.onChanged, + const SizedBox(height: 4), + SizedBox( + height: 24, + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: widget.accentColor, + thumbColor: widget.accentColor, + overlayColor: widget.accentColor.withValues(alpha: 0.12), + inactiveTrackColor: Settings.tacticalVioletTheme.secondary, + trackHeight: 2.8, + mouseCursor: + const WidgetStatePropertyAll(SystemMouseCursors.click), + ), + child: Slider( + min: widget.min, + max: widget.max, + divisions: widget.divisions, + value: widget.value, + onChangeStart: (value) => _dragStartValue = value, + onChangeEnd: (value) { + widget.onChangeCommitted?.call( + _dragStartValue ?? widget.value, + value, + ); + _dragStartValue = null; + }, + onChanged: widget.onChanged, + ), ), ), ], @@ -1451,6 +1303,7 @@ class _SettingsSliderTileState extends State<_SettingsSliderTile> { class _SettingsToggleTile extends StatelessWidget { const _SettingsToggleTile({ required this.icon, + required this.iconColor, required this.title, required this.description, required this.value, @@ -1458,6 +1311,7 @@ class _SettingsToggleTile extends StatelessWidget { }); final IconData icon; + final Color iconColor; final String title; final String description; final bool value; @@ -1468,39 +1322,39 @@ class _SettingsToggleTile extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - _SettingLeadingIcon( - icon: icon, - accentColor: const Color(0xff4b8f86), + SizedBox( + width: 24, + height: 24, + child: Icon( + icon, + size: 20, + color: iconColor, + ), ), - const SizedBox(width: 10), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: const TextStyle(fontWeight: FontWeight.w600), - ), + Text(title), const SizedBox(height: 2), Text( description, style: ShadTheme.of(context).textTheme.small.copyWith( color: Settings.tacticalVioletTheme.mutedForeground, - height: 1.3, + fontSize: 12, + height: 1.35, ), ), ], ), ), - const SizedBox(width: 8), - Padding( - padding: const EdgeInsets.only(top: 2), - child: ShadCheckbox( - value: value, - onChanged: onChanged, - ), + const SizedBox(width: 12), + ShadCheckbox( + value: value, + onChanged: onChanged, ), ], ), @@ -1508,62 +1362,6 @@ class _SettingsToggleTile extends StatelessWidget { } } -class _SettingLeadingIcon extends StatelessWidget { - const _SettingLeadingIcon({ - required this.icon, - required this.accentColor, - }); - - final IconData icon; - final Color accentColor; - - @override - Widget build(BuildContext context) { - return Container( - width: 34, - height: 34, - decoration: BoxDecoration( - color: accentColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: accentColor.withValues(alpha: 0.15), - ), - ), - child: Icon(icon, size: 18, color: accentColor), - ); - } -} - -class _SettingValuePill extends StatelessWidget { - const _SettingValuePill({ - required this.value, - required this.accentColor, - }); - - final String value; - final Color accentColor; - - @override - Widget build(BuildContext context) { - final theme = ShadTheme.of(context); - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), - decoration: BoxDecoration( - color: accentColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(999), - border: Border.all(color: accentColor.withValues(alpha: 0.16)), - ), - child: Text( - value, - style: theme.textTheme.small.copyWith( - color: theme.colorScheme.foreground, - fontWeight: FontWeight.w700, - ), - ), - ); - } -} - class _SettingsItemDivider extends StatelessWidget { const _SettingsItemDivider(); @@ -1571,19 +1369,7 @@ class _SettingsItemDivider extends StatelessWidget { Widget build(BuildContext context) { return Container( height: 1, - color: Settings.tacticalVioletTheme.border.withValues(alpha: 0.8), - ); - } -} - -class _SectionDivider extends StatelessWidget { - const _SectionDivider(); - - @override - Widget build(BuildContext context) { - return Container( - height: 1, - color: Settings.tacticalVioletTheme.border.withValues(alpha: 0.9), + color: Settings.tacticalVioletTheme.border.withValues(alpha: 0.55), ); } }