mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 21:29:36 +02:00
feat(mornye): add persistent accent color choices
This commit is contained in:
1 parent
6b53eb9c06
commit
c89601b6dc
18 files changed
+310
-39
No files matched your search
@@ -36,6 +36,13 @@
|
||||
"description": "System volume control label in the Mornye player"
|
||||
},
|
||||
"appearanceStyle": "App Style",
|
||||
"appearanceAccentRed": "Red (default)",
|
||||
"appearanceAccentOrange": "Orange",
|
||||
"appearanceAccentGreen": "Green",
|
||||
"appearanceAccentTeal": "Teal",
|
||||
"appearanceAccentBlue": "Blue",
|
||||
"appearanceAccentPurple": "Purple",
|
||||
"appearanceAccentPink": "Pink",
|
||||
"@appearanceStyle": {
|
||||
"description": "Appearance section for choosing the app visual style"
|
||||
},
|
||||
|
||||
@@ -36,6 +36,13 @@
|
||||
"description": "Label kontrol volume sistem di pemutar Mornye"
|
||||
},
|
||||
"appearanceStyle": "Gaya Aplikasi",
|
||||
"appearanceAccentRed": "Merah (bawaan)",
|
||||
"appearanceAccentOrange": "Oranye",
|
||||
"appearanceAccentGreen": "Hijau",
|
||||
"appearanceAccentTeal": "Hijau kebiruan",
|
||||
"appearanceAccentBlue": "Biru",
|
||||
"appearanceAccentPurple": "Ungu",
|
||||
"appearanceAccentPink": "Merah muda",
|
||||
"@appearanceStyle": {
|
||||
"description": "Bagian tampilan untuk memilih gaya visual aplikasi"
|
||||
},
|
||||
|
||||
@@ -5,9 +5,18 @@ const String kUseDynamicColorKey = 'use_dynamic_color';
|
||||
const String kSeedColorKey = 'seed_color';
|
||||
const String kUseAmoledKey = 'use_amoled';
|
||||
const String kThemeStyleKey = 'theme_style';
|
||||
const String kMornyeAccentKey = 'mornye_accent';
|
||||
|
||||
enum AppThemeStyle { material, mornye }
|
||||
|
||||
enum MornyeAccent { red, orange, green, teal, blue, purple, pink }
|
||||
|
||||
MornyeAccent mornyeAccentFromString(String? value) =>
|
||||
MornyeAccent.values.firstWhere(
|
||||
(accent) => accent.name == value,
|
||||
orElse: () => MornyeAccent.red,
|
||||
);
|
||||
|
||||
AppThemeStyle themeStyleFromString(String? value) =>
|
||||
AppThemeStyle.values.firstWhere(
|
||||
(style) => style.name == value,
|
||||
@@ -23,6 +32,7 @@ class ThemeSettings {
|
||||
final int seedColorValue;
|
||||
final bool useAmoled;
|
||||
final AppThemeStyle style;
|
||||
final MornyeAccent mornyeAccent;
|
||||
|
||||
const ThemeSettings({
|
||||
this.themeMode = ThemeMode.system,
|
||||
@@ -30,6 +40,7 @@ class ThemeSettings {
|
||||
this.seedColorValue = kDefaultSeedColor,
|
||||
this.useAmoled = false,
|
||||
this.style = AppThemeStyle.material,
|
||||
this.mornyeAccent = MornyeAccent.red,
|
||||
});
|
||||
|
||||
Color get seedColor => Color(seedColorValue);
|
||||
@@ -40,6 +51,7 @@ class ThemeSettings {
|
||||
int? seedColorValue,
|
||||
bool? useAmoled,
|
||||
AppThemeStyle? style,
|
||||
MornyeAccent? mornyeAccent,
|
||||
}) {
|
||||
return ThemeSettings(
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
@@ -47,6 +59,7 @@ class ThemeSettings {
|
||||
seedColorValue: seedColorValue ?? this.seedColorValue,
|
||||
useAmoled: useAmoled ?? this.useAmoled,
|
||||
style: style ?? this.style,
|
||||
mornyeAccent: mornyeAccent ?? this.mornyeAccent,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +69,7 @@ class ThemeSettings {
|
||||
kSeedColorKey: seedColorValue,
|
||||
kUseAmoledKey: useAmoled,
|
||||
kThemeStyleKey: style.name,
|
||||
kMornyeAccentKey: mornyeAccent.name,
|
||||
};
|
||||
|
||||
factory ThemeSettings.fromJson(Map<String, dynamic> json) {
|
||||
@@ -65,6 +79,7 @@ class ThemeSettings {
|
||||
seedColorValue: json[kSeedColorKey] as int? ?? kDefaultSeedColor,
|
||||
useAmoled: json[kUseAmoledKey] as bool? ?? false,
|
||||
style: themeStyleFromString(json[kThemeStyleKey] as String?),
|
||||
mornyeAccent: mornyeAccentFromString(json[kMornyeAccentKey] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +91,8 @@ class ThemeSettings {
|
||||
other.useDynamicColor == useDynamicColor &&
|
||||
other.seedColorValue == seedColorValue &&
|
||||
other.useAmoled == useAmoled &&
|
||||
other.style == style;
|
||||
other.style == style &&
|
||||
other.mornyeAccent == mornyeAccent;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -85,7 +101,8 @@ class ThemeSettings {
|
||||
useDynamicColor.hashCode ^
|
||||
seedColorValue.hashCode ^
|
||||
useAmoled.hashCode ^
|
||||
style.hashCode;
|
||||
style.hashCode ^
|
||||
mornyeAccent.hashCode;
|
||||
}
|
||||
|
||||
ThemeMode themeModeFromString(String? value) {
|
||||
|
||||
@@ -14,6 +14,7 @@ ThemeSettings loadBootstrapThemeSettings(SharedPreferences prefs) {
|
||||
seedColorValue: prefs.getInt(kSeedColorKey) ?? kDefaultSeedColor,
|
||||
useAmoled: prefs.getBool(kUseAmoledKey) ?? false,
|
||||
style: themeStyleFromString(prefs.getString(kThemeStyleKey)),
|
||||
mornyeAccent: mornyeAccentFromString(prefs.getString(kMornyeAccentKey)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,18 +34,7 @@ class ThemeNotifier extends Notifier<ThemeSettings> {
|
||||
Future<void> _loadFromStorage() async {
|
||||
try {
|
||||
final prefs = await _prefs;
|
||||
final modeString = prefs.getString(kThemeModeKey);
|
||||
final useDynamic = prefs.getBool(kUseDynamicColorKey);
|
||||
final seedColor = prefs.getInt(kSeedColorKey);
|
||||
final useAmoled = prefs.getBool(kUseAmoledKey);
|
||||
|
||||
state = ThemeSettings(
|
||||
themeMode: themeModeFromString(modeString),
|
||||
useDynamicColor: useDynamic ?? true,
|
||||
seedColorValue: seedColor ?? kDefaultSeedColor,
|
||||
useAmoled: useAmoled ?? false,
|
||||
style: themeStyleFromString(prefs.getString(kThemeStyleKey)),
|
||||
);
|
||||
state = loadBootstrapThemeSettings(prefs);
|
||||
} catch (e) {
|
||||
debugPrint('Error loading theme settings: $e');
|
||||
}
|
||||
@@ -58,6 +48,7 @@ class ThemeNotifier extends Notifier<ThemeSettings> {
|
||||
await prefs.setInt(kSeedColorKey, state.seedColorValue);
|
||||
await prefs.setBool(kUseAmoledKey, state.useAmoled);
|
||||
await prefs.setString(kThemeStyleKey, state.style.name);
|
||||
await prefs.setString(kMornyeAccentKey, state.mornyeAccent.name);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving theme settings: $e');
|
||||
}
|
||||
@@ -92,4 +83,9 @@ class ThemeNotifier extends Notifier<ThemeSettings> {
|
||||
state = state.copyWith(useAmoled: value);
|
||||
await _saveToStorage();
|
||||
}
|
||||
|
||||
Future<void> setMornyeAccent(MornyeAccent accent) async {
|
||||
state = state.copyWith(mornyeAccent: accent);
|
||||
await _saveToStorage();
|
||||
}
|
||||
}
|
||||
@@ -977,7 +977,10 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen>
|
||||
: colorScheme.surfaceContainerHigh,
|
||||
builder: (sheetContext) => sheetContext.isMornye
|
||||
? Theme(
|
||||
data: MornyeTheme.build(Brightness.dark),
|
||||
data: MornyeTheme.fromContext(
|
||||
sheetContext,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
child: MornyeGlassPanel(
|
||||
tintOpacity: 0.78,
|
||||
child: Builder(builder: buildSheet),
|
||||
|
||||
@@ -83,7 +83,7 @@ class _ConcertDetailScreenState extends State<ConcertDetailScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final source = _detail.coverUrl ?? widget.coverUrl;
|
||||
final base = context.isMornye
|
||||
? MornyeTheme.build(Brightness.dark)
|
||||
? MornyeTheme.fromContext(context, brightness: Brightness.dark)
|
||||
: ThemeData.dark();
|
||||
return Theme(
|
||||
data: base,
|
||||
|
||||
@@ -996,8 +996,10 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
builder: (context, child) => Theme(
|
||||
data: ShellNavigationService.chromeBrightness.value == null
|
||||
? Theme.of(context)
|
||||
: MornyeTheme.build(
|
||||
ShellNavigationService.chromeBrightness.value!,
|
||||
: MornyeTheme.fromContext(
|
||||
context,
|
||||
brightness:
|
||||
ShellNavigationService.chromeBrightness.value!,
|
||||
chromeSurface:
|
||||
ShellNavigationService.chromeSurface.value,
|
||||
),
|
||||
|
||||
@@ -549,7 +549,10 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final mornye = context.isMornye;
|
||||
final colorScheme = mornye
|
||||
? MornyeTheme.build(Brightness.dark).colorScheme
|
||||
? MornyeTheme.fromContext(
|
||||
context,
|
||||
brightness: Brightness.dark,
|
||||
).colorScheme
|
||||
: Theme.of(context).colorScheme;
|
||||
final mediaItem = ref.watch(currentMediaItemProvider).value;
|
||||
final controller = ref.read(musicPlayerControllerProvider);
|
||||
@@ -633,7 +636,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
),
|
||||
fullBleed: key == _motionArtworkKey,
|
||||
child: Theme(
|
||||
data: MornyeTheme.build(Brightness.dark),
|
||||
data: MornyeTheme.fromContext(context, brightness: Brightness.dark),
|
||||
child: key == _compactArtworkKey
|
||||
? PlayerArtwork(
|
||||
artUri: mediaItem.artUri?.toString(),
|
||||
@@ -825,7 +828,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
// exposing a detached rectangle. Keep Mornye artwork inside its panel.
|
||||
enabled: false,
|
||||
child: Theme(
|
||||
data: MornyeTheme.build(Brightness.dark),
|
||||
data: MornyeTheme.fromContext(context, brightness: Brightness.dark),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
|
||||
@@ -98,7 +98,23 @@ class AppearanceSettingsPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
if (themeSettings.style == AppThemeStyle.material) ...[
|
||||
if (themeSettings.style == AppThemeStyle.mornye) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionColor),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
_MornyeAccentPicker(
|
||||
selected: themeSettings.mornyeAccent,
|
||||
onChanged: ref
|
||||
.read(themeProvider.notifier)
|
||||
.setMornyeAccent,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionColor),
|
||||
),
|
||||
@@ -456,6 +472,65 @@ class _ThemePreviewCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _MornyeAccentPicker extends StatelessWidget {
|
||||
const _MornyeAccentPicker({required this.selected, required this.onChanged});
|
||||
|
||||
final MornyeAccent selected;
|
||||
final ValueChanged<MornyeAccent> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final labels = {
|
||||
MornyeAccent.red: l10n.appearanceAccentRed,
|
||||
MornyeAccent.orange: l10n.appearanceAccentOrange,
|
||||
MornyeAccent.green: l10n.appearanceAccentGreen,
|
||||
MornyeAccent.teal: l10n.appearanceAccentTeal,
|
||||
MornyeAccent.blue: l10n.appearanceAccentBlue,
|
||||
MornyeAccent.purple: l10n.appearanceAccentPurple,
|
||||
MornyeAccent.pink: l10n.appearanceAccentPink,
|
||||
};
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final accent in MornyeAccent.values)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final color = MornyeTheme.accentColor(
|
||||
accent,
|
||||
Theme.of(context).brightness,
|
||||
);
|
||||
final foreground =
|
||||
ThemeData.estimateBrightnessForColor(color) ==
|
||||
Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
return Semantics(
|
||||
selected: selected == accent,
|
||||
child: IconButton(
|
||||
tooltip: labels[accent],
|
||||
onPressed: () => onChanged(accent),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: color,
|
||||
foregroundColor: foreground,
|
||||
minimumSize: const Size.square(48),
|
||||
),
|
||||
icon: selected == accent
|
||||
? const Icon(Icons.check)
|
||||
: const SizedBox.square(dimension: 24),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ColorPalettePicker extends StatelessWidget {
|
||||
final int currentColor;
|
||||
final ValueChanged<Color> onColorSelected;
|
||||
|
||||
@@ -325,7 +325,7 @@ class _ThemePicker extends ConsumerWidget {
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Theme(
|
||||
data: style == AppThemeStyle.mornye
|
||||
? MornyeTheme.build(brightness)
|
||||
? MornyeTheme.build(brightness, accent: settings.mornyeAccent)
|
||||
: brightness == Brightness.dark
|
||||
? AppTheme.dark(
|
||||
seedColor: settings.seedColor,
|
||||
|
||||
@@ -19,8 +19,8 @@ class DynamicColorWrapper extends ConsumerWidget {
|
||||
|
||||
if (themeSettings.style == AppThemeStyle.mornye) {
|
||||
return builder(
|
||||
MornyeTheme.build(Brightness.light),
|
||||
MornyeTheme.build(Brightness.dark),
|
||||
MornyeTheme.build(Brightness.light, accent: themeSettings.mornyeAccent),
|
||||
MornyeTheme.build(Brightness.dark, accent: themeSettings.mornyeAccent),
|
||||
themeSettings.themeMode,
|
||||
);
|
||||
}
|
||||
|
||||
+60
-10
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/models/theme_settings.dart';
|
||||
import 'package:spotiflac_android/theme/app_tokens.dart';
|
||||
|
||||
/// Flutter counterpart of Mornye's iPhone visual system. Reference values come
|
||||
@@ -8,9 +9,10 @@ import 'package:spotiflac_android/theme/app_tokens.dart';
|
||||
/// in the Mornye project; keep this palette independent of wallpaper colors.
|
||||
@immutable
|
||||
class MornyeTheme extends ThemeExtension<MornyeTheme> {
|
||||
const MornyeTheme({this.chromeSurface});
|
||||
const MornyeTheme({this.chromeSurface, this.accent = MornyeAccent.red});
|
||||
|
||||
final Color? chromeSurface;
|
||||
final MornyeAccent accent;
|
||||
|
||||
/// A single translucent fill for controls inside an existing glass surface.
|
||||
static Color controlFill(BuildContext context, {bool enabled = true}) {
|
||||
@@ -45,6 +47,23 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
|
||||
static const lightAccent = Color.fromRGBO(204, 46, 51, 1);
|
||||
static const darkAccent = Color.fromRGBO(224, 61, 60, 1);
|
||||
|
||||
static Color accentColor(MornyeAccent accent, Brightness brightness) {
|
||||
final dark = brightness == Brightness.dark;
|
||||
final color = switch (accent) {
|
||||
MornyeAccent.red => const CupertinoDynamicColor.withBrightness(
|
||||
color: lightAccent,
|
||||
darkColor: darkAccent,
|
||||
),
|
||||
MornyeAccent.orange => CupertinoColors.systemOrange,
|
||||
MornyeAccent.green => CupertinoColors.systemGreen,
|
||||
MornyeAccent.teal => CupertinoColors.systemTeal,
|
||||
MornyeAccent.blue => CupertinoColors.systemBlue,
|
||||
MornyeAccent.purple => CupertinoColors.systemPurple,
|
||||
MornyeAccent.pink => CupertinoColors.systemPink,
|
||||
};
|
||||
return dark ? color.darkColor : color.color;
|
||||
}
|
||||
|
||||
static final tokens = AppTokens.standard.copyWith(
|
||||
radiusBadge: 5,
|
||||
radiusThumb: 6,
|
||||
@@ -78,16 +97,24 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
|
||||
// many artists cannot grow the cache indefinitely. Platform is part of the
|
||||
// key because it controls fonts and route transitions.
|
||||
static final _themeCache =
|
||||
<(TargetPlatform, Brightness, Color?), ThemeData>{};
|
||||
<(TargetPlatform, Brightness, Color?, MornyeAccent), ThemeData>{};
|
||||
|
||||
static ThemeData build(Brightness brightness, {Color? chromeSurface}) {
|
||||
final key = (defaultTargetPlatform, brightness, chromeSurface);
|
||||
static ThemeData build(
|
||||
Brightness brightness, {
|
||||
Color? chromeSurface,
|
||||
MornyeAccent accent = MornyeAccent.red,
|
||||
}) {
|
||||
final key = (defaultTargetPlatform, brightness, chromeSurface, accent);
|
||||
final cached = _themeCache.remove(key);
|
||||
if (cached != null) {
|
||||
_themeCache[key] = cached;
|
||||
return cached;
|
||||
}
|
||||
final theme = _build(brightness, chromeSurface: chromeSurface);
|
||||
final theme = _build(
|
||||
brightness,
|
||||
chromeSurface: chromeSurface,
|
||||
selectedAccent: accent,
|
||||
);
|
||||
if (_themeCache.length >= 16) {
|
||||
_themeCache.remove(_themeCache.keys.first);
|
||||
}
|
||||
@@ -95,9 +122,28 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
|
||||
return theme;
|
||||
}
|
||||
|
||||
static ThemeData _build(Brightness brightness, {Color? chromeSurface}) {
|
||||
/// Rebuild local surfaces without losing the user's Mornye preferences.
|
||||
static ThemeData fromContext(
|
||||
BuildContext context, {
|
||||
Brightness? brightness,
|
||||
Color? chromeSurface,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final preferences = theme.extension<MornyeTheme>();
|
||||
return build(
|
||||
brightness ?? theme.brightness,
|
||||
chromeSurface: chromeSurface,
|
||||
accent: preferences?.accent ?? MornyeAccent.red,
|
||||
);
|
||||
}
|
||||
|
||||
static ThemeData _build(
|
||||
Brightness brightness, {
|
||||
Color? chromeSurface,
|
||||
required MornyeAccent selectedAccent,
|
||||
}) {
|
||||
final dark = brightness == Brightness.dark;
|
||||
final accent = dark ? darkAccent : lightAccent;
|
||||
final accent = accentColor(selectedAccent, brightness);
|
||||
final foreground = dark ? Colors.white : Colors.black;
|
||||
final surface = dark ? Colors.black : Colors.white;
|
||||
final grouped = dark ? const Color(0xff1c1c1e) : const Color(0xfff2f2f7);
|
||||
@@ -197,7 +243,7 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
|
||||
fontFamily: text.fontFamily,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
MornyeTheme(chromeSurface: chromeSurface),
|
||||
MornyeTheme(chromeSurface: chromeSurface, accent: selectedAccent),
|
||||
tokens,
|
||||
],
|
||||
);
|
||||
@@ -289,12 +335,16 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
|
||||
}
|
||||
|
||||
@override
|
||||
MornyeTheme copyWith({Color? chromeSurface}) =>
|
||||
MornyeTheme(chromeSurface: chromeSurface ?? this.chromeSurface);
|
||||
MornyeTheme copyWith({Color? chromeSurface, MornyeAccent? accent}) =>
|
||||
MornyeTheme(
|
||||
chromeSurface: chromeSurface ?? this.chromeSurface,
|
||||
accent: accent ?? this.accent,
|
||||
);
|
||||
|
||||
@override
|
||||
MornyeTheme lerp(covariant MornyeTheme? other, double t) => MornyeTheme(
|
||||
chromeSurface: Color.lerp(chromeSurface, other?.chromeSurface, t),
|
||||
accent: t < 0.5 ? accent : other?.accent ?? accent,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,10 @@ class _SpectrogramView extends StatelessWidget {
|
||||
// The plot stays black in both appearances; its controls need matching
|
||||
// contrast even when the surrounding Mornye page uses the light theme.
|
||||
return context.isMornye
|
||||
? Theme(data: MornyeTheme.build(Brightness.dark), child: card)
|
||||
? Theme(
|
||||
data: MornyeTheme.fromContext(context, brightness: Brightness.dark),
|
||||
child: card,
|
||||
)
|
||||
: card;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,10 @@ class _MornyeArtistSurfaceState extends State<MornyeArtistSurface> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final darkTheme = MornyeTheme.build(Brightness.dark);
|
||||
final darkTheme = MornyeTheme.fromContext(
|
||||
context,
|
||||
brightness: Brightness.dark,
|
||||
);
|
||||
return Theme(
|
||||
data: darkTheme.copyWith(
|
||||
colorScheme: darkTheme.colorScheme.copyWith(primary: Colors.grey),
|
||||
|
||||
@@ -172,7 +172,7 @@ class MornyeContextMenu extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = inheritSurface
|
||||
? Theme.of(context)
|
||||
: MornyeTheme.build(Theme.of(context).brightness);
|
||||
: MornyeTheme.fromContext(context);
|
||||
final visibleGroups = groups.where((group) => group.isNotEmpty).toList();
|
||||
final shortcuts = quickActions.isEmpty
|
||||
? null
|
||||
|
||||
@@ -17,7 +17,7 @@ class MornyePlayerNavigationMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = MornyeTheme.build(Brightness.dark);
|
||||
final theme = MornyeTheme.fromContext(context, brightness: Brightness.dark);
|
||||
final art = mediaItem.artUri;
|
||||
final source = art?.scheme == 'file' ? art!.toFilePath() : art?.toString();
|
||||
return Theme(
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/theme_settings.dart';
|
||||
import 'package:spotiflac_android/providers/theme_provider.dart';
|
||||
import 'package:spotiflac_android/screens/settings/appearance_settings_page.dart';
|
||||
import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
|
||||
void main() {
|
||||
Future<void> openSettings(
|
||||
WidgetTester tester,
|
||||
SharedPreferences prefs,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
initialThemeSettingsProvider.overrideWithValue(
|
||||
loadBootstrapThemeSettings(prefs),
|
||||
),
|
||||
],
|
||||
child: DynamicColorWrapper(
|
||||
builder: (light, dark, mode) => MaterialApp(
|
||||
theme: light,
|
||||
darkTheme: dark,
|
||||
themeMode: mode,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const MornyeSettingsTheme(child: AppearanceSettingsPage()),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
for (final mode in [ThemeMode.light, ThemeMode.dark]) {
|
||||
testWidgets('Mornye accent updates live and survives restart ($mode)', (
|
||||
tester,
|
||||
) async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
kThemeStyleKey: 'mornye',
|
||||
kThemeModeKey: mode.name,
|
||||
kSeedColorKey: 0xff123456,
|
||||
});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await openSettings(tester, prefs);
|
||||
final page = find.byType(AppearanceSettingsPage);
|
||||
final initial = Theme.of(tester.element(page));
|
||||
expect(
|
||||
initial.colorScheme.primary,
|
||||
MornyeTheme.accentColor(MornyeAccent.red, initial.brightness),
|
||||
);
|
||||
|
||||
final blue = find.byTooltip('Blue');
|
||||
await tester.ensureVisible(blue);
|
||||
await tester.tap(blue);
|
||||
await tester.pumpAndSettle();
|
||||
final theme = Theme.of(tester.element(page));
|
||||
expect(
|
||||
theme.colorScheme.primary,
|
||||
MornyeTheme.accentColor(MornyeAccent.blue, theme.brightness),
|
||||
);
|
||||
expect(prefs.getString(kMornyeAccentKey), 'blue');
|
||||
expect(prefs.getInt(kSeedColorKey), 0xff123456);
|
||||
final overlay = MornyeTheme.fromContext(
|
||||
tester.element(page),
|
||||
brightness: Brightness.dark,
|
||||
);
|
||||
expect(overlay.extension<MornyeTheme>()!.accent, MornyeAccent.blue);
|
||||
expect(
|
||||
overlay.colorScheme.primary,
|
||||
MornyeTheme.accentColor(MornyeAccent.blue, Brightness.dark),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await openSettings(tester, prefs);
|
||||
expect(
|
||||
Theme.of(tester.element(page)).colorScheme.primary,
|
||||
theme.colorScheme.primary,
|
||||
);
|
||||
await tester.ensureVisible(find.byTooltip('Red (default)'));
|
||||
await tester.tap(find.byTooltip('Red (default)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Theme.of(tester.element(page)).colorScheme.primary,
|
||||
initial.colorScheme.primary,
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1108,6 +1108,7 @@ void main() {
|
||||
seedColorValue: 0xff123456,
|
||||
useAmoled: true,
|
||||
style: AppThemeStyle.mornye,
|
||||
mornyeAccent: MornyeAccent.blue,
|
||||
);
|
||||
|
||||
final decoded = ThemeSettings.fromJson(settings.toJson());
|
||||
@@ -1119,6 +1120,13 @@ void main() {
|
||||
expect(copied.themeMode, ThemeMode.light);
|
||||
expect(copied.useAmoled, isTrue);
|
||||
expect(decoded.style, AppThemeStyle.mornye);
|
||||
expect(decoded.mornyeAccent, MornyeAccent.blue);
|
||||
expect(copied.mornyeAccent, MornyeAccent.blue);
|
||||
expect(ThemeSettings.fromJson({}).mornyeAccent, MornyeAccent.red);
|
||||
expect(
|
||||
ThemeSettings.fromJson({kMornyeAccentKey: 'unknown'}).mornyeAccent,
|
||||
MornyeAccent.red,
|
||||
);
|
||||
expect(
|
||||
ThemeSettings.fromJson({'theme_mode': 'invalid'}).themeMode,
|
||||
ThemeMode.system,
|
||||
|
||||
Reference in new issue
Block a user