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:flutter/material.dart';
|
||||||
|
import 'package:frontend/theme/catppuccin_mocha_theme.dart';
|
||||||
import 'package:frontend/utils/pref_utils.dart';
|
import 'package:frontend/utils/pref_utils.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'navigation.dart';
|
import 'navigation.dart';
|
||||||
|
import 'theme/gruvbox_theme.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
PrefUtil.init();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await PrefUtil.init();
|
||||||
runApp(const MainApp());
|
runApp(const MainApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,20 +18,36 @@ final class MainApp extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider(
|
return ChangeNotifierProvider(
|
||||||
create: (context) => AppState(),
|
create: (context) => AppState(),
|
||||||
child: MaterialApp.router(
|
child: Consumer<AppState>(
|
||||||
title: 'OOTT',
|
builder: (context, appState, _) => MaterialApp.router(
|
||||||
theme: ThemeData(
|
title: 'OOTT',
|
||||||
colorScheme: ColorScheme.fromSeed(
|
theme: appState.theme,
|
||||||
seedColor: Color.fromARGB(255, 214, 93, 14),
|
routerConfig: router,
|
||||||
brightness: Brightness.dark,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
routerConfig: router,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _themes = {
|
||||||
|
'catppuccin_mocha': catppuccinMochaDarkTheme,
|
||||||
|
'gruvbox_dark': gruvboxDarkTheme,
|
||||||
|
};
|
||||||
|
|
||||||
class AppState extends ChangeNotifier {
|
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:encrypter/encrypter/xor.dart';
|
||||||
import 'package:flutter/material.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/oott_api.dart';
|
||||||
import '../utils/pref_utils.dart';
|
import '../utils/pref_utils.dart';
|
||||||
import '../utils/ui_snackbars.dart';
|
import '../utils/ui_snackbars.dart';
|
||||||
@@ -19,6 +22,8 @@ class _SettingsState extends State<Settings> {
|
|||||||
final _apiKeyController = TextEditingController();
|
final _apiKeyController = TextEditingController();
|
||||||
bool _apiKeyVisible = false;
|
bool _apiKeyVisible = false;
|
||||||
bool _testOk = false;
|
bool _testOk = false;
|
||||||
|
bool _connectionModified = false;
|
||||||
|
late String _selectedTheme;
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
@@ -40,6 +45,7 @@ class _SettingsState extends State<Settings> {
|
|||||||
_apiKeyController.text = XOR().xorDecode(
|
_apiKeyController.text = XOR().xorDecode(
|
||||||
PrefUtil.getValue("api_key", "") as String,
|
PrefUtil.getValue("api_key", "") as String,
|
||||||
);
|
);
|
||||||
|
_selectedTheme = context.read<AppState>().themeKey;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
@@ -66,6 +72,7 @@ class _SettingsState extends State<Settings> {
|
|||||||
onChanged: (text) {
|
onChanged: (text) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_testOk = false;
|
_testOk = false;
|
||||||
|
_connectionModified = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
@@ -87,6 +94,7 @@ class _SettingsState extends State<Settings> {
|
|||||||
onChanged: (text) {
|
onChanged: (text) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_testOk = false;
|
_testOk = false;
|
||||||
|
_connectionModified = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -115,6 +123,32 @@ class _SettingsState extends State<Settings> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
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
|
// Button row
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
@@ -130,6 +164,8 @@ class _SettingsState extends State<Settings> {
|
|||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_testOk = testResult == null;
|
_testOk = testResult == null;
|
||||||
|
if (testResult == null)
|
||||||
|
_connectionModified = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (testResult == null) {
|
if (testResult == null) {
|
||||||
@@ -145,10 +181,14 @@ class _SettingsState extends State<Settings> {
|
|||||||
: Icon(Icons.play_arrow),
|
: Icon(Icons.play_arrow),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: _testOk
|
backgroundColor: _testOk
|
||||||
? Colors.lightGreen
|
? Theme.of(
|
||||||
|
context,
|
||||||
|
).extension<AppColorExtension>()!.success
|
||||||
: Theme.of(context).colorScheme.secondary,
|
: Theme.of(context).colorScheme.secondary,
|
||||||
foregroundColor: _testOk
|
foregroundColor: _testOk
|
||||||
? Theme.of(context).colorScheme.onPrimary
|
? Theme.of(
|
||||||
|
context,
|
||||||
|
).extension<AppColorExtension>()!.onSuccess
|
||||||
: Theme.of(context).colorScheme.onSecondary,
|
: Theme.of(context).colorScheme.onSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -156,7 +196,8 @@ class _SettingsState extends State<Settings> {
|
|||||||
|
|
||||||
// Save button
|
// Save button
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: ((!_testOk) || _isSaving)
|
onPressed:
|
||||||
|
((_connectionModified && !_testOk) || _isSaving)
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
@@ -164,6 +205,9 @@ class _SettingsState extends State<Settings> {
|
|||||||
_isSaving = true;
|
_isSaving = true;
|
||||||
});
|
});
|
||||||
_saveData();
|
_saveData();
|
||||||
|
context.read<AppState>().setTheme(
|
||||||
|
_selectedTheme,
|
||||||
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSaving = false;
|
_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 'package:flutter/material.dart';
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
|
||||||
class UISnackbars {
|
class UISnackbars {
|
||||||
static void showError(BuildContext context, String message) {
|
static void showError(BuildContext context, String message) {
|
||||||
@@ -16,15 +17,16 @@ class UISnackbars {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void showSuccess(BuildContext context, String message) {
|
static void showSuccess(BuildContext context, String message) {
|
||||||
|
final appColors = Theme.of(context).extension<AppColorExtension>()!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
message,
|
message,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),
|
style: TextStyle(color: appColors.onSuccess),
|
||||||
),
|
),
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
showCloseIcon: true,
|
showCloseIcon: true,
|
||||||
backgroundColor: Colors.lightGreen,
|
backgroundColor: appColors.success,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user