Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/const/maps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ class Maps {
MapValue.pearl,
];

/// The map's name as users read it: "Ascent", "Icebox".
static String displayName(MapValue map) {
final raw = mapNames[map]!;
return raw[0].toUpperCase() + raw.substring(1);
}

static Map<MapValue, String> mapNames = {
MapValue.ascent: 'ascent',
MapValue.breeze: 'breeze',
Expand Down
25 changes: 22 additions & 3 deletions lib/const/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ class Settings {
// ),
),
));

/// Violet for lines, glyphs, text, and strokes on dark surfaces: two
/// steps lighter than [tacticalVioletTheme.primary], which is the fill
/// under white text and too dark to read as a thin mark.
static const Color accentInk = Color(0xff8b5cf6); // violet-500

static const ShadColorScheme tacticalVioletTheme = ShadColorScheme(
// --- THE GRAYS (UNCHANGED) ---
// These are the "Zinc" cool grays you liked.
Expand All @@ -204,15 +210,16 @@ class Settings {
accent: Color(0xff27272a),
accentForeground: Color(0xfffafafa),
border: Color(0xff27272a),
input: Color(0xff27272a),
// Zinc-700: field edges sit one step above the panel border so a field
// on a card still reads as a field.
input: Color(0xff3f3f46),

// --- THE NEW PURPLE (UPDATED) ---
// Violet-700: Higher contrast, deeper, premium look.
primary: Color(0xff7c3aed),
primaryForeground: Color(0xfff9fafb), // Pure white text pops perfectly here

// Updated ring to match the new primary
ring: Color(0xff7c3aed),
ring: accentInk,

// Selection can stay a bit darker (Violet-800) or match primary
selection: Color(0xff4c1d95),
Expand Down Expand Up @@ -294,6 +301,18 @@ class Settings {
static InsetShadowDecoration raisedPrimary(double radius) =>
raised(tacticalVioletTheme.primary, radius);

/// Dialogs are panels: one surface step above the canvas at the dialog
/// radius, one step above the floating panels, so they read as a sheet
/// from the same family rather than a black box on black.
static final ShadDialogTheme dialogTheme = ShadDialogTheme(
backgroundColor: tacticalVioletTheme.card,
radius: const BorderRadius.all(Radius.circular(16)),
);

/// The destructive fill, raised the same way as the primary one.
static final LinearGradient raisedDestructiveFill =
raisedGradient(tacticalVioletTheme.destructive);

/// The primary fill alone, for the Shad theme and animated fills.
static final LinearGradient raisedPrimaryFill =
raisedGradient(tacticalVioletTheme.primary);
Expand Down
27 changes: 27 additions & 0 deletions lib/const/shortcut_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ enum IcarusShortcutAction {
backwardPage,
addPage,
addLineup,
switchSide,
switchSideThisPage,
openDeleteMenu,
saveStrategy,
pasteImage,
Expand Down Expand Up @@ -276,6 +278,23 @@ class ShortcutInfo {
intent: ToggleLineupIntent(),
searchAliases: ['lineup'],
),
IcarusShortcutDefinition(
action: IcarusShortcutAction.switchSide,
title: 'Switch Side',
defaultBinding: IcarusKeyBinding(trigger: LogicalKeyboardKey.keyF),
intent: SwitchSideIntent(),
searchAliases: ['attack', 'defense', 'defend', 'flip', 'side'],
),
IcarusShortcutDefinition(
action: IcarusShortcutAction.switchSideThisPage,
title: 'Switch Side (This Page)',
defaultBinding: IcarusKeyBinding(
trigger: LogicalKeyboardKey.keyF,
shift: true,
),
intent: SwitchSideThisPageIntent(),
searchAliases: ['attack', 'defense', 'defend', 'flip', 'side', 'page'],
),
IcarusShortcutDefinition(
action: IcarusShortcutAction.openDeleteMenu,
title: 'Open Delete Menu',
Expand Down Expand Up @@ -525,3 +544,11 @@ class ToggleLineupIntent extends Intent {
class OpenInAppDebugIntent extends Intent {
const OpenInAppDebugIntent();
}

class SwitchSideIntent extends Intent {
const SwitchSideIntent();
}

class SwitchSideThisPageIntent extends Intent {
const SwitchSideThisPageIntent();
}
29 changes: 29 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,29 @@ class _MyAppState extends ConsumerState<MyApp> {
brightness: Brightness.dark,
colorScheme: Settings.tacticalVioletTheme,
breadcrumbTheme: const ShadBreadcrumbTheme(separatorSize: 18),
// Dialogs are panels: one surface step above the canvas, so they
// read as a sheet rather than a black box on a black screen.
// Fields keep the panel's surface and take the lighter input
// edge, so they read on a card without becoming a dark well.
inputTheme: ShadInputTheme(
decoration: ShadDecoration(
border: ShadBorder.all(
width: 1,
color: Settings.tacticalVioletTheme.input,
radius: const BorderRadius.all(Radius.circular(6)),
),
),
),
primaryDialogTheme: Settings.dialogTheme,
alertDialogTheme: Settings.dialogTheme,
// Ghost buttons are quiet controls (menu items, icon buttons),
// not primary commands, so they don't get the command color.
ghostButtonTheme: ShadButtonTheme(
foregroundColor: Settings.tacticalVioletTheme.foreground,
),
outlineButtonTheme: ShadButtonTheme(
foregroundColor: Settings.tacticalVioletTheme.foreground,
),
// Primary commands are raised like the selected tab: a lighter top
// of the fill, a bright 1px edge inside the top, a 1px shadow
// beneath. A rounded Border must be one color, so the theme paints
Expand All @@ -381,6 +399,17 @@ class _MyAppState extends ConsumerState<MyApp> {
),
),
),
// Destructive commands are raised the same way, in red.
destructiveButtonTheme: ShadButtonTheme(
decoration: ShadDecoration(
gradient: Settings.raisedDestructiveFill,
shadows: const [Settings.raisedDropShadow],
border: const ShadBorder(
radius: BorderRadius.all(Radius.circular(6)),
top: ShadBorderSide(color: Settings.raisedTopLight, width: 1),
),
),
),
),
home: const MyHomePage(),
routes: {
Expand Down
64 changes: 58 additions & 6 deletions lib/providers/strategy_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3353,7 +3353,15 @@ class StrategyProvider extends Notifier<StrategyState> {
);
}

Future<String> createNewStrategy(String name) async {
/// Creates an empty strategy on [map] and returns it. Without [name] it
/// is auto-named after the map ("Haven", then "Haven 2", ...).
Future<StrategyData> createNewStrategy({
required MapValue map,
String? name,
}) async {
final box = Hive.box<StrategyData>(HiveBoxNames.strategiesBox);
final strategyName = name ??
autoStrategyName(map, box.values.map((strategy) => strategy.name));
final newID = const Uuid().v4();
final pageID = const Uuid().v4();
final defaultThemeProfileId =
Expand All @@ -3366,10 +3374,10 @@ class StrategyProvider extends Notifier<StrategyState> {
appPreferences.defaultNeutralTeamColorsForNewStrategies,
);
final newStrategy = StrategyData(
mapData: MapValue.ascent,
mapData: map,
versionNumber: Settings.versionNumber,
id: newID,
name: name,
name: strategyName,
pages: [
StrategyPage(
id: pageID,
Expand All @@ -3394,12 +3402,11 @@ class StrategyProvider extends Notifier<StrategyState> {
themeProfileId: defaultThemeProfileId,
);

await Hive.box<StrategyData>(HiveBoxNames.strategiesBox)
.put(newStrategy.id, newStrategy);
await box.put(newStrategy.id, newStrategy);

unawaited(AnalyticsService.instance.capture('strategy_created'));

return newStrategy.id;
return newStrategy;
}

void setThemeProfileForCurrentStrategy(String profileId) {
Expand Down Expand Up @@ -4026,6 +4033,38 @@ class StrategyProvider extends Notifier<StrategyState> {
setUnsaved();
}

/// Flips the side the map is drawn from. Placements are stored
/// attack-canonical, so the only thing that changes is each page's side.
/// With [allPages] every page takes the active page's new side; otherwise
/// only the active page changes and the strategy may become mixed.
/// Flips the side of the active page, or of every page when [allPages].
///
/// The active page's side lives in [mapProvider] and reaches Hive through
/// the normal save, so "Don't save" still reverts it and none of its other
/// pending edits are flushed here. The other pages are held in Hive
/// between visits (a page switch writes there the same way), so they are
/// flipped in place.
Future<void> switchSide({required bool allPages}) async {
final isAttack = !ref.read(mapProvider).isAttack;
ref.read(mapProvider.notifier).setAttack(isAttack);
setUnsaved();
if (!allPages || state.stratName == null) return;

final box = Hive.box<StrategyData>(HiveBoxNames.strategiesBox);
final strat = box.get(state.id);
if (strat == null || strat.pages.isEmpty) return;

final activeId = activePageID ?? strat.pages.first.id;
final updated = strat.copyWith(
pages: [
for (final page in strat.pages)
page.id == activeId ? page : page.copyWith(isAttack: isAttack),
],
lastEdited: DateTime.now(),
);
await box.put(updated.id, updated);
}

Future<void> applyNeutralTeamColorsToAllPages(bool value) async {
if (state.stratName == null) return;

Expand Down Expand Up @@ -4068,3 +4107,16 @@ class StrategyProvider extends Notifier<StrategyState> {
}
}
}

/// The name a new strategy gets when the user doesn't type one: the map's
/// name, with the first free number appended if that name is already taken.
@visibleForTesting
String autoStrategyName(MapValue map, Iterable<String> existingNames) {
final base = Maps.displayName(map);
final taken = existingNames.map((name) => name.trim().toLowerCase()).toSet();
if (!taken.contains(base.toLowerCase())) return base;
for (var n = 2;; n++) {
final candidate = '$base $n';
if (!taken.contains(candidate.toLowerCase())) return candidate;
}
}
4 changes: 2 additions & 2 deletions lib/sidebar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ class _SideBarUIState extends ConsumerState<SideBarUI> {
.toggleFavoritesOnly();
},
icon: Icon(
LucideIcons.star600,
size: 24,
LucideIcons.star,
size: 20,
color: filterState.favoritesOnly
? Colors.white
: Settings.tacticalVioletTheme
Expand Down
2 changes: 1 addition & 1 deletion lib/widgets/current_line_up_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class CurrentLineUpPainter extends ConsumerWidget {
child: CustomPaint(
painter: _CurrentLinePainter(
strokeWidth: coordinateSystem.scale(Settings.brushSize),
color: Settings.tacticalVioletTheme.primary,
color: Settings.accentInk,
originAnchor: originAnchor,
landingAnchor: landingAnchor,
dragHover: ref.watch(lineUpDragHoverProvider),
Expand Down
6 changes: 3 additions & 3 deletions lib/widgets/custom_expansion_tile.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:icarus/const/settings.dart';
import 'package:shadcn_ui/shadcn_ui.dart';

class CustomExpansionTile extends StatefulWidget {
Expand Down Expand Up @@ -134,18 +135,17 @@ class _CustomExpansionTileState extends State<CustomExpansionTile>
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;

final Color backgroundColor = _isExpanded
? widget.backgroundColor ?? Colors.transparent
: widget.collapsedBackgroundColor ?? Colors.transparent;

final Color? titleColor = _isExpanded
? widget.textColor ?? colorScheme.primary
? widget.textColor ?? Settings.accentInk
: widget.collapsedTextColor ?? theme.textTheme.titleMedium?.color;

final Color iconColor = _isExpanded
? widget.iconColor ?? colorScheme.primary
? widget.iconColor ?? Settings.accentInk
: widget.collapsedIconColor ?? theme.unselectedWidgetColor;

return Material(
Expand Down
19 changes: 12 additions & 7 deletions lib/widgets/custom_search_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,20 @@ class _SearchTextFieldState extends ConsumerState<SearchTextField> {
decoration: InputDecoration(
isDense: compact,
contentPadding: contentPadding,
// Collapsed, it is a bare icon like the ghost buttons beside it; the
// box only appears once there is a field to type in.
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide:
BorderSide(color: Settings.tacticalVioletTheme.border, width: 1),
borderSide: _expanded
? BorderSide(color: Settings.tacticalVioletTheme.border, width: 1)
: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide:
BorderSide(color: Settings.tacticalVioletTheme.ring, width: 2),
),
filled: true,
filled: _expanded,
fillColor: Settings.tacticalVioletTheme.card,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
Expand All @@ -173,13 +176,15 @@ class _SearchTextFieldState extends ConsumerState<SearchTextField> {
: const EdgeInsets.only(left: 12, right: 8),
child: Icon(
LucideIcons.search,
color: Colors.white,
size: compact ? 18 : 20,
color: _expanded
? Colors.white
: Settings.tacticalVioletTheme.mutedForeground,
size: compact ? 16 : 20,
),
),
prefixIconConstraints: BoxConstraints(
minWidth: compact ? 40 : 40,
minHeight: compact ? 40 : 40,
minWidth: compact ? 28 : 40,
minHeight: compact ? 28 : 40,
),
suffixIcon: _hasText
? IconButton(
Expand Down
4 changes: 2 additions & 2 deletions lib/widgets/desktop_update_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ class _PatchNotes extends StatelessWidget {
case 'warning':
return theme.colorScheme.destructive;
case 'feature':
return theme.colorScheme.primary;
return Settings.accentInk;
default:
return theme.colorScheme.mutedForeground;
}
Expand Down Expand Up @@ -383,7 +383,7 @@ class _DownloadFillButton extends StatelessWidget {
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
width: width * fill,
color: theme.colorScheme.primary,
color: Settings.accentInk,
),
),
Center(
Expand Down
Loading
Loading