mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add theme selection to settings page
Adds a dropdown in the settings page to switch between available themes (Catppuccin Mocha, Gruvbox Dark). The selected theme is applied immediately on save and persisted across restarts via SharedPreferences. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
527e2bd6c8
commit
a647e0d061
+29
-10
@@ -1,10 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend/theme/catppuccin_mocha_theme.dart';
|
||||
import 'package:frontend/utils/pref_utils.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'navigation.dart';
|
||||
import 'theme/gruvbox_theme.dart';
|
||||
|
||||
void main() async {
|
||||
PrefUtil.init();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await PrefUtil.init();
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
@@ -15,20 +18,36 @@ final class MainApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (context) => AppState(),
|
||||
child: MaterialApp.router(
|
||||
title: 'OOTT',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Color.fromARGB(255, 214, 93, 14),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
child: Consumer<AppState>(
|
||||
builder: (context, appState, _) => MaterialApp.router(
|
||||
title: 'OOTT',
|
||||
theme: appState.theme,
|
||||
routerConfig: router,
|
||||
),
|
||||
routerConfig: router,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _themes = {
|
||||
'catppuccin_mocha': catppuccinMochaDarkTheme,
|
||||
'gruvbox_dark': gruvboxDarkTheme,
|
||||
};
|
||||
|
||||
class AppState extends ChangeNotifier {
|
||||
// Global app state goes here
|
||||
AppState() {
|
||||
final saved = PrefUtil.getValue('theme', 'catppuccin_mocha') as String;
|
||||
_themeKey = _themes.containsKey(saved) ? saved : 'catppuccin_mocha';
|
||||
}
|
||||
|
||||
late String _themeKey;
|
||||
String get themeKey => _themeKey;
|
||||
ThemeData get theme => _themes[_themeKey]!;
|
||||
|
||||
void setTheme(String key) {
|
||||
if (!_themes.containsKey(key) || key == _themeKey) return;
|
||||
_themeKey = key;
|
||||
PrefUtil.setValue('theme', key);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:encrypter/encrypter/xor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../utils/oott_api.dart';
|
||||
import '../utils/pref_utils.dart';
|
||||
import '../utils/ui_snackbars.dart';
|
||||
@@ -19,6 +22,8 @@ class _SettingsState extends State<Settings> {
|
||||
final _apiKeyController = TextEditingController();
|
||||
bool _apiKeyVisible = false;
|
||||
bool _testOk = false;
|
||||
bool _connectionModified = false;
|
||||
late String _selectedTheme;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@@ -40,6 +45,7 @@ class _SettingsState extends State<Settings> {
|
||||
_apiKeyController.text = XOR().xorDecode(
|
||||
PrefUtil.getValue("api_key", "") as String,
|
||||
);
|
||||
_selectedTheme = context.read<AppState>().themeKey;
|
||||
_isLoading = false;
|
||||
setState(() {});
|
||||
}
|
||||
@@ -66,6 +72,7 @@ class _SettingsState extends State<Settings> {
|
||||
onChanged: (text) {
|
||||
setState(() {
|
||||
_testOk = false;
|
||||
_connectionModified = true;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
@@ -87,6 +94,7 @@ class _SettingsState extends State<Settings> {
|
||||
onChanged: (text) {
|
||||
setState(() {
|
||||
_testOk = false;
|
||||
_connectionModified = true;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -115,6 +123,32 @@ class _SettingsState extends State<Settings> {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
// Theme selector
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedTheme,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
labelText: 'Theme',
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 'catppuccin_mocha',
|
||||
child: Text('Catppuccin Mocha'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'gruvbox_dark',
|
||||
child: Text('Gruvbox Dark'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedTheme = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
// Button row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@@ -130,6 +164,8 @@ class _SettingsState extends State<Settings> {
|
||||
|
||||
setState(() {
|
||||
_testOk = testResult == null;
|
||||
if (testResult == null)
|
||||
_connectionModified = false;
|
||||
});
|
||||
|
||||
if (testResult == null) {
|
||||
@@ -145,10 +181,14 @@ class _SettingsState extends State<Settings> {
|
||||
: Icon(Icons.play_arrow),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _testOk
|
||||
? Colors.lightGreen
|
||||
? Theme.of(
|
||||
context,
|
||||
).extension<AppColorExtension>()!.success
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: _testOk
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
? Theme.of(
|
||||
context,
|
||||
).extension<AppColorExtension>()!.onSuccess
|
||||
: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
@@ -156,7 +196,8 @@ class _SettingsState extends State<Settings> {
|
||||
|
||||
// Save button
|
||||
ElevatedButton.icon(
|
||||
onPressed: ((!_testOk) || _isSaving)
|
||||
onPressed:
|
||||
((_connectionModified && !_testOk) || _isSaving)
|
||||
? null
|
||||
: () async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
@@ -164,6 +205,9 @@ class _SettingsState extends State<Settings> {
|
||||
_isSaving = true;
|
||||
});
|
||||
_saveData();
|
||||
context.read<AppState>().setTheme(
|
||||
_selectedTheme,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_isSaving = false;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
class AppColorExtension extends ThemeExtension<AppColorExtension> {
|
||||
const AppColorExtension({required this.success, required this.onSuccess});
|
||||
|
||||
final Color success;
|
||||
final Color onSuccess;
|
||||
|
||||
@override
|
||||
AppColorExtension copyWith({Color? success, Color? onSuccess}) =>
|
||||
AppColorExtension(
|
||||
success: success ?? this.success,
|
||||
onSuccess: onSuccess ?? this.onSuccess,
|
||||
);
|
||||
|
||||
@override
|
||||
AppColorExtension lerp(AppColorExtension? other, double t) {
|
||||
if (other is! AppColorExtension) return this;
|
||||
return AppColorExtension(
|
||||
success: Color.lerp(success, other.success, t)!,
|
||||
onSuccess: Color.lerp(onSuccess, other.onSuccess, t)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_colors.dart';
|
||||
|
||||
abstract final class CatppuccinMochaColors {
|
||||
static const Color crust = Color(0xFF11111b);
|
||||
static const Color mantle = Color(0xFF181825);
|
||||
static const Color base = Color(0xFF1e1e2e);
|
||||
static const Color surface0 = Color(0xFF313244);
|
||||
static const Color surface1 = Color(0xFF45475a);
|
||||
static const Color surface2 = Color(0xFF585b70);
|
||||
static const Color overlay0 = Color(0xFF6c7086);
|
||||
static const Color overlay1 = Color(0xFF7f849c);
|
||||
static const Color overlay2 = Color(0xFF9399b2);
|
||||
static const Color subtext0 = Color(0xFFa6adc8);
|
||||
static const Color subtext1 = Color(0xFFbac2de);
|
||||
static const Color text = Color(0xFFcdd6f4);
|
||||
|
||||
static const Color rosewater = Color(0xFFf5e0dc);
|
||||
static const Color flamingo = Color(0xFFf2cdcd);
|
||||
static const Color pink = Color(0xFFf5c2e7);
|
||||
static const Color mauve = Color(0xFFcba6f7);
|
||||
static const Color red = Color(0xFFf38ba8);
|
||||
static const Color maroon = Color(0xFFeba0ac);
|
||||
static const Color peach = Color(0xFFfab387);
|
||||
static const Color yellow = Color(0xFFf9e2af);
|
||||
static const Color green = Color(0xFFa6e3a1);
|
||||
static const Color teal = Color(0xFF94e2d5);
|
||||
static const Color sky = Color(0xFF89dceb);
|
||||
static const Color sapphire = Color(0xFF74c7ec);
|
||||
static const Color blue = Color(0xFF89b4fa);
|
||||
static const Color lavender = Color(0xFFb4befe);
|
||||
}
|
||||
|
||||
const ColorScheme _catppuccinMochaColorScheme = ColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
|
||||
// Surfaces — AppBar (darkest) → NavRail → body (lightest)
|
||||
surface: CatppuccinMochaColors.base,
|
||||
surfaceContainerLowest: CatppuccinMochaColors.crust,
|
||||
surfaceContainerLow: CatppuccinMochaColors.mantle,
|
||||
surfaceContainer: CatppuccinMochaColors.surface0,
|
||||
surfaceContainerHigh: CatppuccinMochaColors.surface1,
|
||||
surfaceContainerHighest: CatppuccinMochaColors.surface2,
|
||||
onSurface: CatppuccinMochaColors.text,
|
||||
onSurfaceVariant: CatppuccinMochaColors.subtext1,
|
||||
outline: CatppuccinMochaColors.overlay1,
|
||||
outlineVariant: CatppuccinMochaColors.overlay0,
|
||||
|
||||
// Primary — Save button, selected nav indicators
|
||||
primary: CatppuccinMochaColors.mauve,
|
||||
onPrimary: CatppuccinMochaColors.crust,
|
||||
primaryContainer: CatppuccinMochaColors.surface0,
|
||||
onPrimaryContainer: CatppuccinMochaColors.text,
|
||||
|
||||
// Secondary — Test button (not-passing state)
|
||||
secondary: CatppuccinMochaColors.blue,
|
||||
onSecondary: CatppuccinMochaColors.crust,
|
||||
secondaryContainer: CatppuccinMochaColors.surface1,
|
||||
onSecondaryContainer: CatppuccinMochaColors.text,
|
||||
|
||||
// Tertiary — swipe-to-unread background
|
||||
tertiary: CatppuccinMochaColors.teal,
|
||||
onTertiary: CatppuccinMochaColors.crust,
|
||||
tertiaryContainer: CatppuccinMochaColors.surface0,
|
||||
onTertiaryContainer: CatppuccinMochaColors.text,
|
||||
|
||||
// Error
|
||||
error: CatppuccinMochaColors.red,
|
||||
onError: CatppuccinMochaColors.crust,
|
||||
errorContainer: CatppuccinMochaColors.maroon,
|
||||
onErrorContainer: CatppuccinMochaColors.crust,
|
||||
|
||||
// Inverse / scrim
|
||||
inverseSurface: CatppuccinMochaColors.text,
|
||||
onInverseSurface: CatppuccinMochaColors.crust,
|
||||
inversePrimary: CatppuccinMochaColors.mauve,
|
||||
scrim: CatppuccinMochaColors.crust,
|
||||
shadow: CatppuccinMochaColors.crust,
|
||||
);
|
||||
|
||||
final ThemeData catppuccinMochaDarkTheme = ThemeData(
|
||||
colorScheme: _catppuccinMochaColorScheme,
|
||||
extensions: [
|
||||
const AppColorExtension(
|
||||
success: CatppuccinMochaColors.green,
|
||||
onSuccess: CatppuccinMochaColors.crust,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_colors.dart';
|
||||
|
||||
abstract final class GruvboxColors {
|
||||
static const Color bgHard = Color(0xFF1d2021);
|
||||
static const Color bg = Color(0xFF282828);
|
||||
static const Color bgSoft = Color(0xFF32302f);
|
||||
static const Color bg1 = Color(0xFF3c3836);
|
||||
static const Color bg2 = Color(0xFF504945);
|
||||
static const Color bg3 = Color(0xFF665c54);
|
||||
static const Color bg4 = Color(0xFF7c6f64);
|
||||
static const Color fg = Color(0xFFebdbb2);
|
||||
static const Color fg2 = Color(0xFFd5c4a1);
|
||||
static const Color gray = Color(0xFF928374);
|
||||
static const Color red = Color(0xFFcc241d);
|
||||
static const Color brightRed = Color(0xFFfb4934);
|
||||
static const Color green = Color(0xFF98971a);
|
||||
static const Color brightGreen = Color(0xFFb8bb26);
|
||||
static const Color yellow = Color(0xFFd79921);
|
||||
static const Color brightYellow = Color(0xFFfabd2f);
|
||||
static const Color blue = Color(0xFF458588);
|
||||
static const Color brightBlue = Color(0xFF83a598);
|
||||
static const Color purple = Color(0xFFb16286);
|
||||
static const Color brightPurple = Color(0xFFd3869b);
|
||||
static const Color aqua = Color(0xFF689d6a);
|
||||
static const Color brightAqua = Color(0xFF8ec07c);
|
||||
static const Color orange = Color(0xFFd65d0e);
|
||||
static const Color brightOrange = Color(0xFFfe8019);
|
||||
}
|
||||
|
||||
const ColorScheme _gruvboxColorScheme = ColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
|
||||
// Surfaces — AppBar (darkest) → NavRail → body (lightest)
|
||||
surface: GruvboxColors.bgSoft,
|
||||
surfaceContainerLowest: GruvboxColors.bgHard,
|
||||
surfaceContainerLow: GruvboxColors.bg,
|
||||
surfaceContainer: GruvboxColors.bg1,
|
||||
surfaceContainerHigh: GruvboxColors.bg1,
|
||||
surfaceContainerHighest: GruvboxColors.bg2,
|
||||
onSurface: GruvboxColors.fg,
|
||||
onSurfaceVariant: GruvboxColors.fg2,
|
||||
outline: GruvboxColors.gray,
|
||||
outlineVariant: GruvboxColors.bg4,
|
||||
|
||||
// Primary — Save button, selected nav indicators
|
||||
primary: GruvboxColors.brightOrange,
|
||||
onPrimary: GruvboxColors.bgHard,
|
||||
primaryContainer: GruvboxColors.bg1,
|
||||
onPrimaryContainer: GruvboxColors.fg,
|
||||
|
||||
// Secondary — Test button (not-passing state)
|
||||
secondary: GruvboxColors.blue,
|
||||
onSecondary: GruvboxColors.fg,
|
||||
secondaryContainer: GruvboxColors.bg2,
|
||||
onSecondaryContainer: GruvboxColors.fg,
|
||||
|
||||
// Tertiary — swipe-to-unread background
|
||||
tertiary: GruvboxColors.aqua,
|
||||
onTertiary: GruvboxColors.bgHard,
|
||||
tertiaryContainer: GruvboxColors.bg1,
|
||||
onTertiaryContainer: GruvboxColors.fg,
|
||||
|
||||
// Error
|
||||
error: GruvboxColors.brightRed,
|
||||
onError: GruvboxColors.bgHard,
|
||||
errorContainer: GruvboxColors.red,
|
||||
onErrorContainer: GruvboxColors.fg,
|
||||
|
||||
// Inverse / scrim
|
||||
inverseSurface: GruvboxColors.fg,
|
||||
onInverseSurface: GruvboxColors.bgHard,
|
||||
inversePrimary: GruvboxColors.orange,
|
||||
scrim: GruvboxColors.bgHard,
|
||||
shadow: GruvboxColors.bgHard,
|
||||
);
|
||||
|
||||
final ThemeData gruvboxDarkTheme = ThemeData(
|
||||
colorScheme: _gruvboxColorScheme,
|
||||
extensions: [
|
||||
const AppColorExtension(
|
||||
success: GruvboxColors.brightGreen,
|
||||
onSuccess: GruvboxColors.bgHard,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
class UISnackbars {
|
||||
static void showError(BuildContext context, String message) {
|
||||
@@ -16,15 +17,16 @@ class UISnackbars {
|
||||
}
|
||||
|
||||
static void showSuccess(BuildContext context, String message) {
|
||||
final appColors = Theme.of(context).extension<AppColorExtension>()!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
message,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),
|
||||
style: TextStyle(color: appColors.onSuccess),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
showCloseIcon: true,
|
||||
backgroundColor: Colors.lightGreen,
|
||||
backgroundColor: appColors.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user