From 1cae561e6c60e22ad71293f3bed69ac04b76d418 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:07:55 +0700 Subject: [PATCH] feat(appearance): make Mornye glass outlines optional --- .../mornye_glass_visual_test.dart | 43 +++++++++++++--- lib/l10n/arb/app_en.arb | 2 + lib/l10n/arb/app_id.arb | 2 + lib/models/theme_settings.dart | 13 ++++- lib/providers/theme_provider.dart | 10 ++++ .../settings/appearance_settings_page.dart | 11 ++++ .../settings/settings_search_catalog.dart | 12 +++++ lib/screens/upgrade_intro_screen.dart | 1 + lib/theme/dynamic_color_wrapper.dart | 2 + lib/theme/mornye_theme.dart | 30 +++++++++-- lib/widgets/mornye_chrome.dart | 27 ++++++---- lib/widgets/mornye_liquid_backdrop.dart | 9 ++-- lib/widgets/mornye_selection_pill.dart | 25 ++++++--- test/appearance_settings_test.dart | 51 +++++++++++++++++++ 14 files changed, 204 insertions(+), 34 deletions(-) diff --git a/integration_test/mornye_glass_visual_test.dart b/integration_test/mornye_glass_visual_test.dart index b032ee37..eca87dae 100644 --- a/integration_test/mornye_glass_visual_test.dart +++ b/integration_test/mornye_glass_visual_test.dart @@ -146,7 +146,13 @@ void main() { await tester.pump(); } - for (final clarity in [0.6, 1.0]) { + for (final (brightness, clarity, outlines) in [ + (Brightness.dark, 0.6, true), + (Brightness.dark, 1.0, true), + (Brightness.light, 0.75, true), + (Brightness.light, 0.75, false), + (Brightness.dark, 0.75, false), + ]) { chrome.expand(); await tester.pumpWidget( ProviderScope( @@ -162,16 +168,22 @@ void main() { mornyeGlassLevelProvider.overrideWithValue(MornyeGlassLevel.liquid), ], child: MaterialApp( - theme: MornyeTheme.build(Brightness.dark, glassClarity: clarity), + theme: MornyeTheme.build( + brightness, + glassClarity: clarity, + glassOutlines: outlines, + ), locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Scaffold( extendBody: true, body: flatBackdrop - ? const ColoredBox( - color: Colors.black, - child: SizedBox.expand(), + ? ColoredBox( + color: brightness == Brightness.dark + ? Colors.black + : Colors.white, + child: const SizedBox.expand(), ) : const CustomPaint( painter: _Pattern(), @@ -229,7 +241,15 @@ void main() { } expect(find.byType(MiniPlayer), findsOneWidget); final player = tester.state(find.byType(MiniPlayer)); - final prefix = 'clarity_${(clarity * 100).round()}'; + final prefix = + '${brightness.name}_clarity_${(clarity * 100).round()}_outlines_$outlines'; + final lenses = tester.widgetList( + find.byType(LiquidGlassLens), + ); + expect(lenses, isNotEmpty); + for (final lens in lenses) { + expect(lens.style.shape?.borderWidth, outlines ? 0.75 : 0); + } await capture('${prefix}_expanded'); await halfway(true); await capture('${prefix}_folding_half'); @@ -331,6 +351,15 @@ void main() { await touch.cancel(); } await wait(300); + // Live emulator frames can lag behind the release animation's duration. + for ( + var attempt = 0; + capsule() != restingCapsule && attempt < 20; + attempt++ + ) { + await wait(100); + await tester.pump(); + } expect(capsule(), restingCapsule); expect(foreground(), restingForeground); expect(tester.state(find.byType(MiniPlayer)), same(player)); @@ -339,7 +368,7 @@ void main() { binding.reportData!['mornye_glass_visual'] = { 'scope': 'Production MornyeBottomBar/MiniPlayer over ' - '${flatBackdrop ? 'flat black' : 'generated stripes/grid'}; ' + '${flatBackdrop ? 'flat light/dark surfaces' : 'generated stripes/grid'}; ' 'paused in-memory playback and settings, no persistent data or network.', 'captureSurface': '${Platform.isAndroid ? 'Android converted Flutter image view' : 'iOS window'}; ' diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index bdeb8530..c9b2ddb4 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -114,6 +114,8 @@ "appearanceAccentPurple": "Purple", "appearanceAccentPink": "Pink", "appearanceGlassClarity": "Glass clarity", + "appearanceGlassOutlines": "Glass outlines", + "appearanceGlassOutlinesSubtitle": "Show decorative edges around Mornye glass. Turning this off keeps blur and refraction.", "appearanceGlassClaritySubtitle": "Adjust transparency in Mornye menus and navigation. Text contrast is preserved.", "appearanceGlassTinted": "Tinted", "appearanceGlassClear": "Clear", diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 8dc5b123..cfb721aa 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -198,6 +198,8 @@ "appearanceAccentPurple": "Ungu", "appearanceAccentPink": "Merah muda", "appearanceGlassClarity": "Kebeningan kaca", + "appearanceGlassOutlines": "Garis tepi kaca", + "appearanceGlassOutlinesSubtitle": "Tampilkan garis dekoratif di tepi kaca Mornye. Mematikannya tetap mempertahankan blur dan pembiasan.", "appearanceGlassClaritySubtitle": "Atur kebeningan kaca menu dan navigasi Mornye. Kontras teks tetap terjaga.", "appearanceGlassTinted": "Berwarna", "appearanceGlassClear": "Bening", diff --git a/lib/models/theme_settings.dart b/lib/models/theme_settings.dart index a7de8517..9baad713 100644 --- a/lib/models/theme_settings.dart +++ b/lib/models/theme_settings.dart @@ -8,6 +8,7 @@ const String kThemeStyleKey = 'theme_style'; const String kMornyeAccentKey = 'mornye_accent'; const String kUseSystemFontKey = 'use_system_font'; const String kMornyeGlassClarityKey = 'mornye_glass_clarity'; +const String kMornyeGlassOutlinesKey = 'mornye_glass_outlines'; const double kDefaultMornyeGlassClarity = 0.75; double normalizeMornyeGlassClarity(num? value) => @@ -43,6 +44,7 @@ class ThemeSettings { final MornyeAccent mornyeAccent; final bool useSystemFont; final double mornyeGlassClarity; + final bool mornyeGlassOutlines; const ThemeSettings({ this.themeMode = ThemeMode.system, @@ -53,6 +55,7 @@ class ThemeSettings { this.mornyeAccent = MornyeAccent.red, this.useSystemFont = false, this.mornyeGlassClarity = kDefaultMornyeGlassClarity, + this.mornyeGlassOutlines = true, }); Color get seedColor => Color(seedColorValue); @@ -66,6 +69,7 @@ class ThemeSettings { MornyeAccent? mornyeAccent, bool? useSystemFont, double? mornyeGlassClarity, + bool? mornyeGlassOutlines, }) { return ThemeSettings( themeMode: themeMode ?? this.themeMode, @@ -78,6 +82,7 @@ class ThemeSettings { mornyeGlassClarity: normalizeMornyeGlassClarity( mornyeGlassClarity ?? this.mornyeGlassClarity, ), + mornyeGlassOutlines: mornyeGlassOutlines ?? this.mornyeGlassOutlines, ); } @@ -90,6 +95,7 @@ class ThemeSettings { kMornyeAccentKey: mornyeAccent.name, kUseSystemFontKey: useSystemFont, kMornyeGlassClarityKey: mornyeGlassClarity, + kMornyeGlassOutlinesKey: mornyeGlassOutlines, }; factory ThemeSettings.fromJson(Map json) { @@ -104,6 +110,7 @@ class ThemeSettings { mornyeGlassClarity: normalizeMornyeGlassClarity( json[kMornyeGlassClarityKey] as num?, ), + mornyeGlassOutlines: json[kMornyeGlassOutlinesKey] as bool? ?? true, ); } @@ -118,7 +125,8 @@ class ThemeSettings { other.style == style && other.mornyeAccent == mornyeAccent && other.useSystemFont == useSystemFont && - other.mornyeGlassClarity == mornyeGlassClarity; + other.mornyeGlassClarity == mornyeGlassClarity && + other.mornyeGlassOutlines == mornyeGlassOutlines; } @override @@ -130,7 +138,8 @@ class ThemeSettings { style.hashCode ^ mornyeAccent.hashCode ^ useSystemFont.hashCode ^ - mornyeGlassClarity.hashCode; + mornyeGlassClarity.hashCode ^ + mornyeGlassOutlines.hashCode; } ThemeMode themeModeFromString(String? value) { diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart index 7938bec4..f867c477 100644 --- a/lib/providers/theme_provider.dart +++ b/lib/providers/theme_provider.dart @@ -19,6 +19,7 @@ ThemeSettings loadBootstrapThemeSettings(SharedPreferences prefs) { mornyeGlassClarity: normalizeMornyeGlassClarity( prefs.getDouble(kMornyeGlassClarityKey), ), + mornyeGlassOutlines: prefs.getBool(kMornyeGlassOutlinesKey) ?? true, ); } @@ -80,6 +81,10 @@ class ThemeNotifier extends Notifier { kMornyeGlassClarityKey, snapshot.mornyeGlassClarity, ); + await prefs.setBool( + kMornyeGlassOutlinesKey, + snapshot.mornyeGlassOutlines, + ); } catch (e) { debugPrint('Error saving theme settings: $e'); } @@ -127,4 +132,9 @@ class ThemeNotifier extends Notifier { _editState((current) => current.copyWith(mornyeGlassClarity: value)); if (persist) await _saveToStorage(); } + + Future setMornyeGlassOutlines(bool value) async { + _editState((current) => current.copyWith(mornyeGlassOutlines: value)); + await _saveToStorage(); + } } diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index 1b96df0c..97791180 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -113,6 +113,17 @@ class AppearanceSettingsPage extends ConsumerWidget { .setMornyeAccent, ), const _MornyeGlassClaritySlider(), + SettingsSwitchItem( + key: const ValueKey('mornye-glass-outlines'), + icon: Icons.rounded_corner, + title: context.l10n.appearanceGlassOutlines, + subtitle: context.l10n.appearanceGlassOutlinesSubtitle, + value: themeSettings.mornyeGlassOutlines, + onChanged: ref + .read(themeProvider.notifier) + .setMornyeGlassOutlines, + showDivider: false, + ), ], ), ), diff --git a/lib/screens/settings/settings_search_catalog.dart b/lib/screens/settings/settings_search_catalog.dart index 22f2d7c6..94fb745a 100644 --- a/lib/screens/settings/settings_search_catalog.dart +++ b/lib/screens/settings/settings_search_catalog.dart @@ -89,6 +89,18 @@ class SettingsSearchCatalog { ), ], appearance = [ + SettingsSearchEntry( + icon: Icons.rounded_corner, + title: l10n.appearanceGlassOutlines, + subtitle: l10n.appearanceGlassOutlinesSubtitle, + keywords: const [ + 'mornye', + 'liquid glass', + 'border', + 'rim', + 'outline', + ], + ), SettingsSearchEntry( icon: Icons.wallpaper, title: l10n.appearanceDynamicColor, diff --git a/lib/screens/upgrade_intro_screen.dart b/lib/screens/upgrade_intro_screen.dart index 95634860..c3d7ac1b 100644 --- a/lib/screens/upgrade_intro_screen.dart +++ b/lib/screens/upgrade_intro_screen.dart @@ -330,6 +330,7 @@ class _ThemePicker extends ConsumerWidget { accent: settings.mornyeAccent, useSystemFont: settings.useSystemFont, glassClarity: settings.mornyeGlassClarity, + glassOutlines: settings.mornyeGlassOutlines, ) : brightness == Brightness.dark ? AppTheme.dark( diff --git a/lib/theme/dynamic_color_wrapper.dart b/lib/theme/dynamic_color_wrapper.dart index 21eeed02..ae381f2d 100644 --- a/lib/theme/dynamic_color_wrapper.dart +++ b/lib/theme/dynamic_color_wrapper.dart @@ -24,12 +24,14 @@ class DynamicColorWrapper extends ConsumerWidget { accent: themeSettings.mornyeAccent, useSystemFont: themeSettings.useSystemFont, glassClarity: themeSettings.mornyeGlassClarity, + glassOutlines: themeSettings.mornyeGlassOutlines, ), MornyeTheme.build( Brightness.dark, accent: themeSettings.mornyeAccent, useSystemFont: themeSettings.useSystemFont, glassClarity: themeSettings.mornyeGlassClarity, + glassOutlines: themeSettings.mornyeGlassOutlines, ), themeSettings.themeMode, ); diff --git a/lib/theme/mornye_theme.dart b/lib/theme/mornye_theme.dart index be4f29aa..5867db03 100644 --- a/lib/theme/mornye_theme.dart +++ b/lib/theme/mornye_theme.dart @@ -14,17 +14,22 @@ class MornyeTheme extends ThemeExtension { this.accent = MornyeAccent.red, this.useSystemFont = false, this.glassClarity = kDefaultMornyeGlassClarity, + this.glassOutlines = true, }); final Color? chromeSurface; final MornyeAccent accent; final bool useSystemFont; final double glassClarity; + final bool glassOutlines; static double glassClarityOf(BuildContext context) => Theme.of(context).extension()?.glassClarity ?? kDefaultMornyeGlassClarity; + static bool glassOutlinesOf(BuildContext context) => + Theme.of(context).extension()?.glassOutlines ?? true; + /// A single translucent fill for controls inside an existing glass surface. static Color controlFill(BuildContext context, {bool enabled = true}) { final theme = Theme.of(context); @@ -121,6 +126,7 @@ class MornyeTheme extends ThemeExtension { MornyeAccent accent = MornyeAccent.red, bool useSystemFont = false, double glassClarity = kDefaultMornyeGlassClarity, + bool glassOutlines = true, }) { final key = ( defaultTargetPlatform, @@ -132,7 +138,7 @@ class MornyeTheme extends ThemeExtension { final cached = _themeCache.remove(key); if (cached != null) { _themeCache[key] = cached; - return _withGlassClarity(cached, glassClarity); + return _withGlassPreferences(cached, glassClarity, glassOutlines); } final theme = _build( brightness, @@ -144,19 +150,27 @@ class MornyeTheme extends ThemeExtension { _themeCache.remove(_themeCache.keys.first); } _themeCache[key] = theme; - return _withGlassClarity(theme, glassClarity); + return _withGlassPreferences(theme, glassClarity, glassOutlines); } // Moving the clarity slider only changes the material, not typography or // seeded colors. Keep the expensive base theme cached independently. - static ThemeData _withGlassClarity(ThemeData theme, double value) { + static ThemeData _withGlassPreferences( + ThemeData theme, + double value, + bool outlines, + ) { final clarity = normalizeMornyeGlassClarity(value); - if (theme.extension()!.glassClarity == clarity) return theme; + final preferences = theme.extension()!; + if (preferences.glassClarity == clarity && + preferences.glassOutlines == outlines) { + return theme; + } return theme.copyWith( extensions: [ for (final extension in theme.extensions.values) if (extension is MornyeTheme) - extension.copyWith(glassClarity: clarity) + extension.copyWith(glassClarity: clarity, glassOutlines: outlines) else extension, ], @@ -177,6 +191,7 @@ class MornyeTheme extends ThemeExtension { accent: preferences?.accent ?? MornyeAccent.red, useSystemFont: preferences?.useSystemFont ?? false, glassClarity: preferences?.glassClarity ?? kDefaultMornyeGlassClarity, + glassOutlines: preferences?.glassOutlines ?? true, ); } @@ -394,11 +409,13 @@ class MornyeTheme extends ThemeExtension { MornyeAccent? accent, bool? useSystemFont, double? glassClarity, + bool? glassOutlines, }) => MornyeTheme( chromeSurface: chromeSurface ?? this.chromeSurface, accent: accent ?? this.accent, useSystemFont: useSystemFont ?? this.useSystemFont, glassClarity: glassClarity ?? this.glassClarity, + glassOutlines: glassOutlines ?? this.glassOutlines, ); @override @@ -411,6 +428,9 @@ class MornyeTheme extends ThemeExtension { glassClarity: glassClarity + ((other?.glassClarity ?? glassClarity) - glassClarity) * t, + glassOutlines: t < 0.5 + ? glassOutlines + : other?.glassOutlines ?? glassOutlines, ); } diff --git a/lib/widgets/mornye_chrome.dart b/lib/widgets/mornye_chrome.dart index f475c004..7655a602 100644 --- a/lib/widgets/mornye_chrome.dart +++ b/lib/widgets/mornye_chrome.dart @@ -184,6 +184,7 @@ class MornyeGlass extends ConsumerWidget { clarity > 0 && !MediaQuery.highContrastOf(context); final dark = scheme.brightness == Brightness.dark; + final outlines = MornyeTheme.glassOutlinesOf(context); final shape = BorderRadius.vertical( top: firstInGroup ? Radius.circular(radius) : Radius.zero, bottom: lastInGroup ? Radius.circular(radius) : Radius.zero, @@ -280,13 +281,18 @@ class MornyeGlass extends ConsumerWidget { : null, ), child: CustomPaint( - foregroundPainter: _MornyeGlassRim( - shape: shape, - firstInGroup: firstInGroup, - lastInGroup: lastInGroup, - illuminated: translucent && (!useLens || highlightBorder), - emphasized: highlightBorder, - ), + // The optical lens already draws its own rim. Avoid a second dark + // contact stroke on top, particularly visible against light pages. + foregroundPainter: outlines && (!useLens || highlightBorder) + ? _MornyeGlassRim( + shape: shape, + firstInGroup: firstInGroup, + lastInGroup: lastInGroup, + illuminated: translucent && (!useLens || highlightBorder), + emphasized: highlightBorder, + dark: dark, + ) + : null, child: ClipRRect( borderRadius: shape, child: useLens @@ -324,6 +330,7 @@ class _MornyeGlassRim extends CustomPainter { required this.lastInGroup, required this.illuminated, required this.emphasized, + required this.dark, }); final BorderRadius shape; @@ -331,6 +338,7 @@ class _MornyeGlassRim extends CustomPainter { final bool lastInGroup; final bool illuminated; final bool emphasized; + final bool dark; @override void paint(Canvas canvas, Size size) { @@ -352,7 +360,7 @@ class _MornyeGlassRim extends CustomPainter { ..style = PaintingStyle.stroke ..strokeWidth = 0.5 // A thin contact edge must not paint a shadow under the whole glass. - ..color = Colors.black.withValues(alpha: 0.5), + ..color = Colors.black.withValues(alpha: dark ? 0.5 : 0.16), ); if (illuminated) { canvas.drawRRect( @@ -387,7 +395,8 @@ class _MornyeGlassRim extends CustomPainter { firstInGroup != oldDelegate.firstInGroup || lastInGroup != oldDelegate.lastInGroup || illuminated != oldDelegate.illuminated || - emphasized != oldDelegate.emphasized; + emphasized != oldDelegate.emphasized || + dark != oldDelegate.dark; } /// A searchable category uses the same material as the navigation capsule. diff --git a/lib/widgets/mornye_liquid_backdrop.dart b/lib/widgets/mornye_liquid_backdrop.dart index 30001852..e51d7e9d 100644 --- a/lib/widgets/mornye_liquid_backdrop.dart +++ b/lib/widgets/mornye_liquid_backdrop.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:liquid_glass_easy/liquid_glass_easy.dart'; +import 'package:spotiflac_android/theme/mornye_theme.dart'; /// A bounded package lens without a captured-screen view or a touch ticker. class MornyeLiquidBackdrop extends StatelessWidget { @@ -28,14 +29,16 @@ class MornyeLiquidBackdrop extends StatelessWidget { Widget build(BuildContext context) { if (interaction && progress <= 0) return child; final effect = interaction ? progress : 1.0; + final outlines = MornyeTheme.glassOutlinesOf(context); + final dark = Theme.of(context).brightness == Brightness.dark; return LiquidGlassLens( style: LiquidGlassStyle( shape: LiquidGlassShape.roundedRectangle( cornerRadius: borderRadius.topLeft.x, - borderWidth: 0.75, + borderWidth: outlines ? 0.75 : 0, lightDirection: 0, - lightIntensity: 0.25 * effect, - lightColor: Colors.white, + lightIntensity: outlines ? (dark ? 0.25 : 0.12) * effect : 0, + lightColor: dark ? Colors.white : const Color(0x88ffffff), borderType: const OpticalBorder( ambientIntensity: 0.05, lightSpread: 0.2, diff --git a/lib/widgets/mornye_selection_pill.dart b/lib/widgets/mornye_selection_pill.dart index 7a9814aa..cbbe61a8 100644 --- a/lib/widgets/mornye_selection_pill.dart +++ b/lib/widgets/mornye_selection_pill.dart @@ -5,6 +5,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:liquid_glass_easy/liquid_glass_easy.dart'; +import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/widgets/mornye_liquid_backdrop.dart'; Future? _selectionMaskProgram; @@ -247,10 +248,14 @@ class _MornyeSelectionPillState extends State : 0.08, ), borderRadius: BorderRadius.circular(_pillRadius), - border: Border.all( - color: scheme.onSurface.withValues(alpha: 0.05), - width: 0.5, - ), + border: MornyeTheme.glassOutlinesOf(context) + ? Border.all( + color: scheme.onSurface.withValues( + alpha: 0.05, + ), + width: 0.5, + ) + : null, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), @@ -408,10 +413,14 @@ class _MornyeSelectionPillState extends State ? 0.24 : 0.08), ), - border: Border.all( - color: Colors.white.withValues(alpha: 0.6 * progress), - width: 0.75, - ), + border: MornyeTheme.glassOutlinesOf(context) + ? Border.all( + color: Colors.white.withValues( + alpha: 0.6 * progress, + ), + width: 0.75, + ) + : null, ), child: const SizedBox.expand(), ), diff --git a/test/appearance_settings_test.dart b/test/appearance_settings_test.dart index c250f9b9..a2ab1ed0 100644 --- a/test/appearance_settings_test.dart +++ b/test/appearance_settings_test.dart @@ -11,6 +11,19 @@ import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart'; void main() { + test( + 'old themes retain outlines and new themes serialize the preference', + () { + expect(ThemeSettings.fromJson({}).mornyeGlassOutlines, isTrue); + const settings = ThemeSettings(mornyeGlassOutlines: false); + expect(ThemeSettings.fromJson(settings.toJson()), settings); + expect( + settings.copyWith(mornyeGlassClarity: 1).mornyeGlassOutlines, + isFalse, + ); + expect(settings, isNot(const ThemeSettings())); + }, + ); test('glass clarity restores safely from old and invalid settings', () { expect( ThemeSettings.fromJson({}).mornyeGlassClarity, @@ -90,6 +103,44 @@ void main() { } for (final mode in [ThemeMode.light, ThemeMode.dark]) { + testWidgets( + 'glass outlines toggle survives restart and local themes ($mode)', + (tester) async { + SharedPreferences.setMockInitialValues({ + kThemeStyleKey: 'mornye', + kThemeModeKey: mode.name, + }); + final prefs = await SharedPreferences.getInstance(); + await openSettings(tester, prefs); + final toggle = find.byKey(const ValueKey('mornye-glass-outlines')); + await tester.ensureVisible(toggle); + await tester.pumpAndSettle(); + await tester.tap(find.text('Glass outlines')); + await tester.pumpAndSettle(); + expect(prefs.getBool(kMornyeGlassOutlinesKey), isFalse); + final context = tester.element(toggle); + expect(MornyeTheme.glassOutlinesOf(context), isFalse); + final local = MornyeTheme.fromContext( + context, + brightness: Brightness.dark, + ); + expect(local.extension()!.glassOutlines, isFalse); + expect( + local.extension()!.glassClarity, + kDefaultMornyeGlassClarity, + ); + await tester.pumpWidget(const SizedBox()); + await openSettings(tester, prefs); + expect(MornyeTheme.glassOutlinesOf(tester.element(toggle)), isFalse); + await tester.ensureVisible(toggle); + await tester.pumpAndSettle(); + await tester.tap(find.text('Glass outlines')); + await tester.pumpAndSettle(); + expect(prefs.getBool(kMornyeGlassOutlinesKey), isTrue); + expect(tester.takeException(), isNull); + }, + ); + testWidgets('glass clarity previews live and survives restart ($mode)', ( tester, ) async {