diff --git a/TODO.md b/TODO.md index c9d205c..c234d28 100644 --- a/TODO.md +++ b/TODO.md @@ -29,7 +29,7 @@ ## Frontend -- [ ] Add more themes (Dracula, Nord, Catppuccin Latte, Gruvbox light) +- [x] Add more themes (Dracula, Nord, Catppuccin Latte, Gruvbox light) - [x] Remove the license and notice from the UI, just link to the github pages - [x] On Android, make the font of the icon (phone screen) bigger - [x] In the status screen, the "listening for" property of passive scanners should change to days and months (now its always minutes) diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 059706c..a2bff3d 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -2,7 +2,12 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:frontend/theme/alucard_theme.dart'; +import 'package:frontend/theme/catppuccin_latte_theme.dart'; import 'package:frontend/theme/catppuccin_mocha_theme.dart'; +import 'package:frontend/theme/dracula_theme.dart'; +import 'package:frontend/theme/nord_theme.dart'; +import 'package:frontend/theme/tokyo_night_theme.dart'; import 'package:frontend/utils/local_network_permission.dart'; import 'package:frontend/utils/pref_utils.dart'; import 'package:frontend/utils/push_service.dart'; @@ -75,8 +80,13 @@ final class MainApp extends StatelessWidget { } final _themes = { + 'alucard': alucardLightTheme, + 'catppuccin_latte': catppuccinLatteLightTheme, 'catppuccin_mocha': catppuccinMochaDarkTheme, + 'dracula': draculaDarkTheme, 'gruvbox_dark': gruvboxDarkTheme, + 'nord': nordDarkTheme, + 'tokyo_night': tokyoNightDarkTheme, }; class AppState extends ChangeNotifier { diff --git a/frontend/lib/settings/settings.dart b/frontend/lib/settings/settings.dart index 2878032..7c3f2b7 100644 --- a/frontend/lib/settings/settings.dart +++ b/frontend/lib/settings/settings.dart @@ -292,14 +292,25 @@ class _SettingsState extends State { labelText: 'Theme', ), items: const [ + DropdownMenuItem(value: 'alucard', child: Text('Alucard')), + DropdownMenuItem( + value: 'catppuccin_latte', + child: Text('Catppuccin Latte'), + ), DropdownMenuItem( value: 'catppuccin_mocha', child: Text('Catppuccin Mocha'), ), + DropdownMenuItem(value: 'dracula', child: Text('Dracula')), DropdownMenuItem( value: 'gruvbox_dark', child: Text('Gruvbox Dark'), ), + DropdownMenuItem(value: 'nord', child: Text('Nord')), + DropdownMenuItem( + value: 'tokyo_night', + child: Text('Tokyo Night'), + ), ], // Applies immediately; the theme persists itself, no Save needed. onChanged: (value) { diff --git a/frontend/lib/theme/alucard_theme.dart b/frontend/lib/theme/alucard_theme.dart new file mode 100644 index 0000000..a74c31e --- /dev/null +++ b/frontend/lib/theme/alucard_theme.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'app_colors.dart'; +import 'theme_builder.dart'; + +/// Alucard — the official light counterpart to Dracula. +abstract final class AlucardColors { + // Neutral ramp (lightest → darkest). Background, the warm/cream tints and the + // darker gray are from the base24 Alucard palette; the two mid surfaces are + // interpolated to give cards consistent elevation steps. + static const Color background = Color(0xFFfffbeb); + static const Color warm = Color(0xFFfdf8e2); + static const Color cream = Color(0xFFf5f0da); + static const Color surface = Color(0xFFf0ebd5); + static const Color surfaceHigh = Color(0xFFded8c2); + static const Color gray = Color(0xFFceccc0); + static const Color comment = Color(0xFF6c664b); + static const Color subtleForeground = Color(0xFF4c4a3d); + static const Color foreground = Color(0xFF1f1f1f); + + static const Color cyan = Color(0xFF036a96); + static const Color green = Color(0xFF14710a); + static const Color orange = Color(0xFFa34d14); + static const Color pink = Color(0xFFa3144d); + static const Color purple = Color(0xFF644ac9); + static const Color brightPurple = Color(0xFF7862d0); + static const Color red = Color(0xFFcb3a2a); + static const Color darkRed = Color(0xFF792219); + static const Color redContainer = Color(0xFFf5d6d1); + static const Color yellow = Color(0xFF846e15); +} + +const ColorScheme _alucardColorScheme = ColorScheme( + brightness: Brightness.light, + + // Surfaces — AppBar (darkest) → NavRail → body (lightest) + surface: AlucardColors.background, + surfaceContainerLowest: AlucardColors.cream, + surfaceContainerLow: AlucardColors.warm, + surfaceContainer: AlucardColors.surface, + surfaceContainerHigh: AlucardColors.surfaceHigh, + surfaceContainerHighest: AlucardColors.gray, + onSurface: AlucardColors.foreground, + onSurfaceVariant: AlucardColors.subtleForeground, + outline: AlucardColors.comment, + outlineVariant: AlucardColors.gray, + + // Primary — Save button, selected nav indicators + primary: AlucardColors.purple, + onPrimary: AlucardColors.background, + primaryContainer: AlucardColors.surfaceHigh, + onPrimaryContainer: AlucardColors.foreground, + + // Secondary — Test button (not-passing state) + secondary: AlucardColors.cyan, + onSecondary: AlucardColors.background, + secondaryContainer: AlucardColors.gray, + onSecondaryContainer: AlucardColors.foreground, + + // Tertiary — swipe-to-unread background + tertiary: AlucardColors.green, + onTertiary: AlucardColors.background, + tertiaryContainer: AlucardColors.surface, + onTertiaryContainer: AlucardColors.foreground, + + // Error + error: AlucardColors.red, + onError: AlucardColors.background, + errorContainer: AlucardColors.redContainer, + onErrorContainer: AlucardColors.darkRed, + + // Inverse / scrim + inverseSurface: AlucardColors.foreground, + onInverseSurface: AlucardColors.background, + inversePrimary: AlucardColors.brightPurple, + scrim: AlucardColors.foreground, + shadow: AlucardColors.comment, +); + +final ThemeData alucardLightTheme = buildAppTheme( + colorScheme: _alucardColorScheme, + appColors: const AppColorExtension( + success: AlucardColors.green, + onSuccess: AlucardColors.background, + warning: AlucardColors.orange, + onWarning: AlucardColors.background, + info: AlucardColors.cyan, + onInfo: AlucardColors.background, + ), +); diff --git a/frontend/lib/theme/catppuccin_latte_theme.dart b/frontend/lib/theme/catppuccin_latte_theme.dart new file mode 100644 index 0000000..0fb4a6f --- /dev/null +++ b/frontend/lib/theme/catppuccin_latte_theme.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'app_colors.dart'; +import 'theme_builder.dart'; + +abstract final class CatppuccinLatteColors { + static const Color crust = Color(0xFFdce0e8); + static const Color mantle = Color(0xFFe6e9ef); + static const Color base = Color(0xFFeff1f5); + static const Color surface0 = Color(0xFFccd0da); + static const Color surface1 = Color(0xFFbcc0cc); + static const Color surface2 = Color(0xFFacb0be); + static const Color overlay0 = Color(0xFF9ca0b0); + static const Color overlay1 = Color(0xFF8c8fa1); + static const Color overlay2 = Color(0xFF7c7f93); + static const Color subtext0 = Color(0xFF6c6f85); + static const Color subtext1 = Color(0xFF5c5f77); + static const Color text = Color(0xFF4c4f69); + + static const Color rosewater = Color(0xFFdc8a78); + static const Color flamingo = Color(0xFFdd7878); + static const Color pink = Color(0xFFea76cb); + static const Color mauve = Color(0xFF8839ef); + static const Color red = Color(0xFFd20f39); + static const Color maroon = Color(0xFFe64553); + static const Color peach = Color(0xFFfe640b); + static const Color yellow = Color(0xFFdf8e1d); + static const Color green = Color(0xFF40a02b); + static const Color teal = Color(0xFF179299); + static const Color sky = Color(0xFF04a5e5); + static const Color sapphire = Color(0xFF209fb5); + static const Color blue = Color(0xFF1e66f5); + static const Color lavender = Color(0xFF7287fd); +} + +const ColorScheme _catppuccinLatteColorScheme = ColorScheme( + brightness: Brightness.light, + + // Surfaces — AppBar (darkest) → NavRail → body (lightest) + surface: CatppuccinLatteColors.base, + surfaceContainerLowest: CatppuccinLatteColors.crust, + surfaceContainerLow: CatppuccinLatteColors.mantle, + surfaceContainer: CatppuccinLatteColors.surface0, + surfaceContainerHigh: CatppuccinLatteColors.surface1, + surfaceContainerHighest: CatppuccinLatteColors.surface2, + onSurface: CatppuccinLatteColors.text, + onSurfaceVariant: CatppuccinLatteColors.subtext1, + outline: CatppuccinLatteColors.overlay1, + outlineVariant: CatppuccinLatteColors.overlay0, + + // Primary — Save button, selected nav indicators + primary: CatppuccinLatteColors.mauve, + onPrimary: CatppuccinLatteColors.base, + primaryContainer: CatppuccinLatteColors.surface0, + onPrimaryContainer: CatppuccinLatteColors.text, + + // Secondary — Test button (not-passing state) + secondary: CatppuccinLatteColors.blue, + onSecondary: CatppuccinLatteColors.base, + secondaryContainer: CatppuccinLatteColors.surface1, + onSecondaryContainer: CatppuccinLatteColors.text, + + // Tertiary — swipe-to-unread background + tertiary: CatppuccinLatteColors.teal, + onTertiary: CatppuccinLatteColors.base, + tertiaryContainer: CatppuccinLatteColors.surface0, + onTertiaryContainer: CatppuccinLatteColors.text, + + // Error + error: CatppuccinLatteColors.red, + onError: CatppuccinLatteColors.base, + errorContainer: CatppuccinLatteColors.maroon, + onErrorContainer: CatppuccinLatteColors.base, + + // Inverse / scrim + inverseSurface: CatppuccinLatteColors.text, + onInverseSurface: CatppuccinLatteColors.base, + inversePrimary: CatppuccinLatteColors.mauve, + scrim: CatppuccinLatteColors.crust, + shadow: CatppuccinLatteColors.text, +); + +final ThemeData catppuccinLatteLightTheme = buildAppTheme( + colorScheme: _catppuccinLatteColorScheme, + appColors: const AppColorExtension( + success: CatppuccinLatteColors.green, + onSuccess: CatppuccinLatteColors.base, + warning: CatppuccinLatteColors.peach, + onWarning: CatppuccinLatteColors.base, + info: CatppuccinLatteColors.blue, + onInfo: CatppuccinLatteColors.base, + ), +); diff --git a/frontend/lib/theme/dracula_theme.dart b/frontend/lib/theme/dracula_theme.dart new file mode 100644 index 0000000..dbd3250 --- /dev/null +++ b/frontend/lib/theme/dracula_theme.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'app_colors.dart'; +import 'theme_builder.dart'; + +abstract final class DraculaColors { + // Neutral ramp (darkest → lightest). "black" and "currentLine" come from the + // official palette; the intermediate elevated surfaces are interpolated. + static const Color blacker = Color(0xFF1b1c24); + static const Color black = Color(0xFF21222c); + static const Color background = Color(0xFF282a36); + static const Color elevated = Color(0xFF343746); + static const Color currentLine = Color(0xFF44475a); + static const Color overlay = Color(0xFF565872); + static const Color comment = Color(0xFF6272a4); + static const Color subtleForeground = Color(0xFFb8bbd0); + static const Color foreground = Color(0xFFf8f8f2); + + static const Color cyan = Color(0xFF8be9fd); + static const Color green = Color(0xFF50fa7b); + static const Color orange = Color(0xFFffb86c); + static const Color pink = Color(0xFFff79c6); + static const Color purple = Color(0xFFbd93f9); + static const Color red = Color(0xFFff5555); + static const Color brightRed = Color(0xFFff6e6e); + static const Color yellow = Color(0xFFf1fa8c); +} + +const ColorScheme _draculaColorScheme = ColorScheme( + brightness: Brightness.dark, + + // Surfaces — AppBar (darkest) → NavRail → body (lightest) + surface: DraculaColors.background, + surfaceContainerLowest: DraculaColors.blacker, + surfaceContainerLow: DraculaColors.black, + surfaceContainer: DraculaColors.elevated, + surfaceContainerHigh: DraculaColors.currentLine, + surfaceContainerHighest: DraculaColors.overlay, + onSurface: DraculaColors.foreground, + onSurfaceVariant: DraculaColors.subtleForeground, + outline: DraculaColors.comment, + outlineVariant: DraculaColors.currentLine, + + // Primary — Save button, selected nav indicators + primary: DraculaColors.purple, + onPrimary: DraculaColors.background, + primaryContainer: DraculaColors.currentLine, + onPrimaryContainer: DraculaColors.foreground, + + // Secondary — Test button (not-passing state) + secondary: DraculaColors.cyan, + onSecondary: DraculaColors.background, + secondaryContainer: DraculaColors.elevated, + onSecondaryContainer: DraculaColors.foreground, + + // Tertiary — swipe-to-unread background + tertiary: DraculaColors.pink, + onTertiary: DraculaColors.background, + tertiaryContainer: DraculaColors.currentLine, + onTertiaryContainer: DraculaColors.foreground, + + // Error + error: DraculaColors.red, + onError: DraculaColors.background, + errorContainer: DraculaColors.brightRed, + onErrorContainer: DraculaColors.background, + + // Inverse / scrim + inverseSurface: DraculaColors.foreground, + onInverseSurface: DraculaColors.background, + inversePrimary: DraculaColors.purple, + scrim: DraculaColors.black, + shadow: DraculaColors.black, +); + +final ThemeData draculaDarkTheme = buildAppTheme( + colorScheme: _draculaColorScheme, + appColors: const AppColorExtension( + success: DraculaColors.green, + onSuccess: DraculaColors.background, + warning: DraculaColors.orange, + onWarning: DraculaColors.background, + info: DraculaColors.cyan, + onInfo: DraculaColors.background, + ), +); diff --git a/frontend/lib/theme/nord_theme.dart b/frontend/lib/theme/nord_theme.dart new file mode 100644 index 0000000..f4d8933 --- /dev/null +++ b/frontend/lib/theme/nord_theme.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'app_colors.dart'; +import 'theme_builder.dart'; + +abstract final class NordColors { + // Polar Night (nord0–nord3). nord0Dark is interpolated below nord0 to give + // the AppBar a slightly deeper shade than the body. + static const Color nord0Dark = Color(0xFF2b303b); + static const Color nord0 = Color(0xFF2e3440); + static const Color nord0Soft = Color(0xFF353c4a); + static const Color nord1 = Color(0xFF3b4252); + static const Color nord2 = Color(0xFF434c5e); + static const Color nord3 = Color(0xFF4c566a); + + // Snow Storm (nord4–nord6) + static const Color nord4 = Color(0xFFd8dee9); + static const Color nord5 = Color(0xFFe5e9f0); + static const Color nord6 = Color(0xFFeceff4); + + // Frost (nord7–nord10) + static const Color nord7 = Color(0xFF8fbcbb); + static const Color nord8 = Color(0xFF88c0d0); + static const Color nord9 = Color(0xFF81a1c1); + static const Color nord10 = Color(0xFF5e81ac); + + // Aurora (nord11–nord15) + static const Color nord11 = Color(0xFFbf616a); + static const Color nord12 = Color(0xFFd08770); + static const Color nord13 = Color(0xFFebcb8b); + static const Color nord14 = Color(0xFFa3be8c); + static const Color nord15 = Color(0xFFb48ead); +} + +const ColorScheme _nordColorScheme = ColorScheme( + brightness: Brightness.dark, + + // Surfaces — AppBar (darkest) → NavRail → body (lightest) + surface: NordColors.nord0, + surfaceContainerLowest: NordColors.nord0Dark, + surfaceContainerLow: NordColors.nord0Soft, + surfaceContainer: NordColors.nord1, + surfaceContainerHigh: NordColors.nord2, + surfaceContainerHighest: NordColors.nord3, + onSurface: NordColors.nord6, + onSurfaceVariant: NordColors.nord4, + outline: NordColors.nord3, + outlineVariant: NordColors.nord2, + + // Primary — Save button, selected nav indicators + primary: NordColors.nord8, + onPrimary: NordColors.nord0, + primaryContainer: NordColors.nord1, + onPrimaryContainer: NordColors.nord6, + + // Secondary — Test button (not-passing state) + secondary: NordColors.nord9, + onSecondary: NordColors.nord0, + secondaryContainer: NordColors.nord2, + onSecondaryContainer: NordColors.nord6, + + // Tertiary — swipe-to-unread background + tertiary: NordColors.nord7, + onTertiary: NordColors.nord0, + tertiaryContainer: NordColors.nord1, + onTertiaryContainer: NordColors.nord6, + + // Error + error: NordColors.nord11, + onError: NordColors.nord6, + errorContainer: NordColors.nord11, + onErrorContainer: NordColors.nord0, + + // Inverse / scrim + inverseSurface: NordColors.nord6, + onInverseSurface: NordColors.nord0, + inversePrimary: NordColors.nord10, + scrim: NordColors.nord0Dark, + shadow: NordColors.nord0Dark, +); + +final ThemeData nordDarkTheme = buildAppTheme( + colorScheme: _nordColorScheme, + appColors: const AppColorExtension( + success: NordColors.nord14, + onSuccess: NordColors.nord0, + warning: NordColors.nord13, + onWarning: NordColors.nord0, + info: NordColors.nord8, + onInfo: NordColors.nord0, + ), +); diff --git a/frontend/lib/theme/theme_builder.dart b/frontend/lib/theme/theme_builder.dart index b236f58..f88d52b 100644 --- a/frontend/lib/theme/theme_builder.dart +++ b/frontend/lib/theme/theme_builder.dart @@ -6,8 +6,9 @@ import 'app_colors.dart'; /// Builds a Material 3 [ThemeData] from a [ColorScheme] and the app's custom /// [AppColorExtension], applying the shared OOTT typography. /// -/// Both bundled themes (Catppuccin Mocha, Gruvbox Dark) go through here so they -/// stay visually consistent — only their colors differ. +/// Every bundled theme (Catppuccin Mocha/Latte, Gruvbox Dark, Dracula, Alucard, +/// Nord, Tokyo Night) goes through here so they stay visually consistent — only +/// their colors differ. ThemeData buildAppTheme({ required ColorScheme colorScheme, required AppColorExtension appColors, diff --git a/frontend/lib/theme/tokyo_night_theme.dart b/frontend/lib/theme/tokyo_night_theme.dart new file mode 100644 index 0000000..8a4e78f --- /dev/null +++ b/frontend/lib/theme/tokyo_night_theme.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'app_colors.dart'; +import 'theme_builder.dart'; + +/// Tokyo Night (the "Night" variant). +abstract final class TokyoNightColors { + static const Color bgDarker = Color(0xFF0c0e14); + static const Color bgDark = Color(0xFF16161e); + static const Color bg = Color(0xFF1a1b26); + static const Color bgSoft = Color(0xFF1b1e2d); + static const Color bgElevated = Color(0xFF24283b); + static const Color bgHighlight = Color(0xFF292e42); + static const Color fgGutter = Color(0xFF3b4261); + static const Color terminalBlack = Color(0xFF414868); + static const Color comment = Color(0xFF565f89); + static const Color fgDark = Color(0xFFa9b1d6); + static const Color fg = Color(0xFFc0caf5); + + static const Color blue = Color(0xFF7aa2f7); + static const Color cyan = Color(0xFF7dcfff); + static const Color teal = Color(0xFF73daca); + static const Color green = Color(0xFF9ece6a); + static const Color magenta = Color(0xFFbb9af7); + static const Color purple = Color(0xFF9d7cd8); + static const Color orange = Color(0xFFff9e64); + static const Color yellow = Color(0xFFe0af68); + static const Color red = Color(0xFFf7768e); + static const Color brightRed = Color(0xFFdb4b4b); +} + +const ColorScheme _tokyoNightColorScheme = ColorScheme( + brightness: Brightness.dark, + + // Surfaces — AppBar (darkest) → NavRail → body (lightest) + surface: TokyoNightColors.bg, + surfaceContainerLowest: TokyoNightColors.bgDark, + surfaceContainerLow: TokyoNightColors.bgSoft, + surfaceContainer: TokyoNightColors.bgElevated, + surfaceContainerHigh: TokyoNightColors.bgHighlight, + surfaceContainerHighest: TokyoNightColors.fgGutter, + onSurface: TokyoNightColors.fg, + onSurfaceVariant: TokyoNightColors.fgDark, + outline: TokyoNightColors.comment, + outlineVariant: TokyoNightColors.terminalBlack, + + // Primary — Save button, selected nav indicators + primary: TokyoNightColors.blue, + onPrimary: TokyoNightColors.bgDark, + primaryContainer: TokyoNightColors.bgHighlight, + onPrimaryContainer: TokyoNightColors.fg, + + // Secondary — Test button (not-passing state) + secondary: TokyoNightColors.magenta, + onSecondary: TokyoNightColors.bgDark, + secondaryContainer: TokyoNightColors.fgGutter, + onSecondaryContainer: TokyoNightColors.fg, + + // Tertiary — swipe-to-unread background + tertiary: TokyoNightColors.teal, + onTertiary: TokyoNightColors.bgDark, + tertiaryContainer: TokyoNightColors.bgElevated, + onTertiaryContainer: TokyoNightColors.fg, + + // Error + error: TokyoNightColors.red, + onError: TokyoNightColors.bgDark, + errorContainer: TokyoNightColors.brightRed, + onErrorContainer: TokyoNightColors.fg, + + // Inverse / scrim + inverseSurface: TokyoNightColors.fg, + onInverseSurface: TokyoNightColors.bgDark, + inversePrimary: TokyoNightColors.blue, + scrim: TokyoNightColors.bgDarker, + shadow: TokyoNightColors.bgDarker, +); + +final ThemeData tokyoNightDarkTheme = buildAppTheme( + colorScheme: _tokyoNightColorScheme, + appColors: const AppColorExtension( + success: TokyoNightColors.green, + onSuccess: TokyoNightColors.bgDark, + warning: TokyoNightColors.yellow, + onWarning: TokyoNightColors.bgDark, + info: TokyoNightColors.blue, + onInfo: TokyoNightColors.bgDark, + ), +);