add oled theme support
This commit is contained in:
@@ -114,6 +114,35 @@ class AppColors extends ThemeExtension<AppColors> {
|
|||||||
brandLink: Color(0xFFFBDC6B),
|
brandLink: Color(0xFFFBDC6B),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Pure-black ("OLED") variant of [dark]. Identical to [dark] except the
|
||||||
|
/// decorative aura tones collapse toward black so the home/new-tab backdrop
|
||||||
|
/// reads as black instead of charcoal. Semantic colors (private, isolated,
|
||||||
|
/// tor, brand, warning) are kept so they stay legible. The aura orbs are
|
||||||
|
/// heavily blurred, so the faint tints below still render as a subtle brand
|
||||||
|
/// glow on black rather than flat dead black.
|
||||||
|
static const darkOled = AppColors._(
|
||||||
|
seedColor: Color(0xFF167C80),
|
||||||
|
privateTabPurple: Color(0xFF8000D7),
|
||||||
|
privateTabBackground: Color(0xFF25003E),
|
||||||
|
privateTabForeground: Color(0xFFFFFFFF),
|
||||||
|
privateSelectionOverlay: Color(0x648000D7),
|
||||||
|
isolatedTabTeal: Color(0xFF00897B),
|
||||||
|
isolatedTabBackground: Color(0xFF003D36),
|
||||||
|
isolatedTabForeground: Color(0xFFFFFFFF),
|
||||||
|
isolatedSelectionOverlay: Color(0x6400897B),
|
||||||
|
torPurple: Color(0xFF7D4698),
|
||||||
|
torActiveGreen: Color(0xFF68B030),
|
||||||
|
torBackgroundGrey: Color(0xFF333A41),
|
||||||
|
warningAmber: Color(0xFFFFA000),
|
||||||
|
// Aura: collapsed toward black, keeping only a whisper of brand tint.
|
||||||
|
auraPurple: Color(0xFF0E0A1A),
|
||||||
|
auraGold: Color(0xFF17130A),
|
||||||
|
auraShadow: Color(0xFF050505),
|
||||||
|
auraShadowHighlight: Color(0xFF0C0C0E),
|
||||||
|
auraTint: Color(0xFF000000),
|
||||||
|
brandLink: Color(0xFFFBDC6B),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AppColors copyWith({
|
AppColors copyWith({
|
||||||
Color? seedColor,
|
Color? seedColor,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabStatesProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabStatesHash() => r'd7efef406dd132a3ef1a67da06c794d18ccb484a';
|
String _$tabStatesHash() => r'04b7f1ea704575e368a0f1186fc1c58f317620b1';
|
||||||
|
|
||||||
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
||||||
Map<String, TabState> build();
|
Map<String, TabState> build();
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabDataRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabDataRepositoryHash() => r'38750c2e0d27b407b1038d324ad22eeb202da6eb';
|
String _$tabDataRepositoryHash() => r'5192e4bb2a373bdef4474fb781eaacc64cc35609';
|
||||||
|
|
||||||
abstract class _$TabDataRepository extends $Notifier<void> {
|
abstract class _$TabDataRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ const List<SettingsSectionDefinition> generalSettingsSections = [
|
|||||||
keywords: ['light', 'dark', 'theme mode'],
|
keywords: ['light', 'dark', 'theme mode'],
|
||||||
child: _ThemeSection(),
|
child: _ThemeSection(),
|
||||||
),
|
),
|
||||||
|
SettingsEntryDefinition(
|
||||||
|
title: 'Pure Black (OLED)',
|
||||||
|
subtitle: 'Use true-black surfaces in dark mode to save power on OLED '
|
||||||
|
'screens',
|
||||||
|
keywords: ['oled', 'amoled', 'high contrast', 'black', 'dark'],
|
||||||
|
child: _PureBlackTile(),
|
||||||
|
),
|
||||||
SettingsEntryDefinition(
|
SettingsEntryDefinition(
|
||||||
title: 'User Interface Zoom',
|
title: 'User Interface Zoom',
|
||||||
subtitle: 'Make the user interface smaller or larger',
|
subtitle: 'Make the user interface smaller or larger',
|
||||||
@@ -275,6 +282,43 @@ class _ShowModalBarrierTile extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PureBlackTile extends HookConsumerWidget {
|
||||||
|
const _PureBlackTile();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final pureBlack = ref.watch(
|
||||||
|
generalSettingsWithDefaultsProvider.select((s) => s.pureBlack),
|
||||||
|
);
|
||||||
|
final themeMode = ref.watch(
|
||||||
|
generalSettingsWithDefaultsProvider.select((s) => s.themeMode),
|
||||||
|
);
|
||||||
|
|
||||||
|
// OLED surfaces only apply to dark mode; disable the toggle when the app
|
||||||
|
// is locked to light mode so the setting can't appear to have no effect.
|
||||||
|
final enabled = themeMode != ThemeMode.light;
|
||||||
|
|
||||||
|
return SwitchListTile.adaptive(
|
||||||
|
title: const Text('Pure Black (OLED)'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Use true-black surfaces in dark mode to save power on OLED screens',
|
||||||
|
),
|
||||||
|
secondary: const Icon(Icons.contrast),
|
||||||
|
value: pureBlack,
|
||||||
|
onChanged: enabled
|
||||||
|
? (value) async {
|
||||||
|
await ref
|
||||||
|
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||||
|
.save(
|
||||||
|
(currentSettings) =>
|
||||||
|
currentSettings.copyWith.pureBlack(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ThemeSection extends HookConsumerWidget {
|
class _ThemeSection extends HookConsumerWidget {
|
||||||
const _ThemeSection();
|
const _ThemeSection();
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ class GeneralSettings with FastEquatable {
|
|||||||
/// accept and complete an inline search suggestion. Defaults to false.
|
/// accept and complete an inline search suggestion. Defaults to false.
|
||||||
final bool acceptSuggestionOnSubmit;
|
final bool acceptSuggestionOnSubmit;
|
||||||
|
|
||||||
|
/// Whether dark mode should use pure-black ("OLED"/high-contrast) surfaces.
|
||||||
|
/// Only takes effect when the effective brightness is dark. Defaults to false.
|
||||||
|
final bool pureBlack;
|
||||||
|
|
||||||
GeneralSettings({
|
GeneralSettings({
|
||||||
required this.themeMode,
|
required this.themeMode,
|
||||||
required this.uiScaleFactor,
|
required this.uiScaleFactor,
|
||||||
@@ -203,6 +207,7 @@ class GeneralSettings with FastEquatable {
|
|||||||
required this.enableLocalSearchIndex,
|
required this.enableLocalSearchIndex,
|
||||||
required this.indexPrivateTabs,
|
required this.indexPrivateTabs,
|
||||||
required this.acceptSuggestionOnSubmit,
|
required this.acceptSuggestionOnSubmit,
|
||||||
|
required this.pureBlack,
|
||||||
});
|
});
|
||||||
|
|
||||||
GeneralSettings.withDefaults({
|
GeneralSettings.withDefaults({
|
||||||
@@ -263,6 +268,7 @@ class GeneralSettings with FastEquatable {
|
|||||||
bool? enableLocalSearchIndex,
|
bool? enableLocalSearchIndex,
|
||||||
bool? indexPrivateTabs,
|
bool? indexPrivateTabs,
|
||||||
bool? acceptSuggestionOnSubmit,
|
bool? acceptSuggestionOnSubmit,
|
||||||
|
bool? pureBlack,
|
||||||
}) : themeMode = themeMode ?? ThemeMode.dark,
|
}) : themeMode = themeMode ?? ThemeMode.dark,
|
||||||
uiScaleFactor = uiScaleFactor ?? defaultUiScaleFactor,
|
uiScaleFactor = uiScaleFactor ?? defaultUiScaleFactor,
|
||||||
disableAnimations = disableAnimations ?? false,
|
disableAnimations = disableAnimations ?? false,
|
||||||
@@ -331,7 +337,8 @@ class GeneralSettings with FastEquatable {
|
|||||||
externalAppIntentPolicies = externalAppIntentPolicies ?? const {},
|
externalAppIntentPolicies = externalAppIntentPolicies ?? const {},
|
||||||
enableLocalSearchIndex = enableLocalSearchIndex ?? true,
|
enableLocalSearchIndex = enableLocalSearchIndex ?? true,
|
||||||
indexPrivateTabs = indexPrivateTabs ?? false,
|
indexPrivateTabs = indexPrivateTabs ?? false,
|
||||||
acceptSuggestionOnSubmit = acceptSuggestionOnSubmit ?? false;
|
acceptSuggestionOnSubmit = acceptSuggestionOnSubmit ?? false,
|
||||||
|
pureBlack = pureBlack ?? false;
|
||||||
|
|
||||||
factory GeneralSettings.fromJson(Map<String, dynamic> json) {
|
factory GeneralSettings.fromJson(Map<String, dynamic> json) {
|
||||||
// Migrate legacy `newTabPosition` setting to direction settings.
|
// Migrate legacy `newTabPosition` setting to direction settings.
|
||||||
@@ -442,5 +449,6 @@ class GeneralSettings with FastEquatable {
|
|||||||
enableLocalSearchIndex,
|
enableLocalSearchIndex,
|
||||||
indexPrivateTabs,
|
indexPrivateTabs,
|
||||||
acceptSuggestionOnSubmit,
|
acceptSuggestionOnSubmit,
|
||||||
|
pureBlack,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ abstract class _$GeneralSettingsCWProxy {
|
|||||||
|
|
||||||
GeneralSettings acceptSuggestionOnSubmit(bool acceptSuggestionOnSubmit);
|
GeneralSettings acceptSuggestionOnSubmit(bool acceptSuggestionOnSubmit);
|
||||||
|
|
||||||
|
GeneralSettings pureBlack(bool pureBlack);
|
||||||
|
|
||||||
/// Creates a new instance with the provided field values.
|
/// Creates a new instance with the provided field values.
|
||||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
|
||||||
///
|
///
|
||||||
@@ -208,6 +210,7 @@ abstract class _$GeneralSettingsCWProxy {
|
|||||||
bool enableLocalSearchIndex,
|
bool enableLocalSearchIndex,
|
||||||
bool indexPrivateTabs,
|
bool indexPrivateTabs,
|
||||||
bool acceptSuggestionOnSubmit,
|
bool acceptSuggestionOnSubmit,
|
||||||
|
bool pureBlack,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,6 +464,9 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
GeneralSettings acceptSuggestionOnSubmit(bool acceptSuggestionOnSubmit) =>
|
GeneralSettings acceptSuggestionOnSubmit(bool acceptSuggestionOnSubmit) =>
|
||||||
call(acceptSuggestionOnSubmit: acceptSuggestionOnSubmit);
|
call(acceptSuggestionOnSubmit: acceptSuggestionOnSubmit);
|
||||||
|
|
||||||
|
@override
|
||||||
|
GeneralSettings pureBlack(bool pureBlack) => call(pureBlack: pureBlack);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// Creates a new instance with the provided field values.
|
/// Creates a new instance with the provided field values.
|
||||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
|
||||||
@@ -528,6 +534,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
Object? enableLocalSearchIndex = const $CopyWithPlaceholder(),
|
Object? enableLocalSearchIndex = const $CopyWithPlaceholder(),
|
||||||
Object? indexPrivateTabs = const $CopyWithPlaceholder(),
|
Object? indexPrivateTabs = const $CopyWithPlaceholder(),
|
||||||
Object? acceptSuggestionOnSubmit = const $CopyWithPlaceholder(),
|
Object? acceptSuggestionOnSubmit = const $CopyWithPlaceholder(),
|
||||||
|
Object? pureBlack = const $CopyWithPlaceholder(),
|
||||||
}) {
|
}) {
|
||||||
return GeneralSettings(
|
return GeneralSettings(
|
||||||
themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null
|
themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null
|
||||||
@@ -866,6 +873,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
? _value.acceptSuggestionOnSubmit
|
? _value.acceptSuggestionOnSubmit
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: acceptSuggestionOnSubmit as bool,
|
: acceptSuggestionOnSubmit as bool,
|
||||||
|
pureBlack: pureBlack == const $CopyWithPlaceholder() || pureBlack == null
|
||||||
|
? _value.pureBlack
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: pureBlack as bool,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -992,6 +1003,7 @@ GeneralSettings _$GeneralSettingsFromJson(
|
|||||||
enableLocalSearchIndex: json['enableLocalSearchIndex'] as bool?,
|
enableLocalSearchIndex: json['enableLocalSearchIndex'] as bool?,
|
||||||
indexPrivateTabs: json['indexPrivateTabs'] as bool?,
|
indexPrivateTabs: json['indexPrivateTabs'] as bool?,
|
||||||
acceptSuggestionOnSubmit: json['acceptSuggestionOnSubmit'] as bool?,
|
acceptSuggestionOnSubmit: json['acceptSuggestionOnSubmit'] as bool?,
|
||||||
|
pureBlack: json['pureBlack'] as bool?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$GeneralSettingsToJson(
|
Map<String, dynamic> _$GeneralSettingsToJson(
|
||||||
@@ -1067,6 +1079,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(
|
|||||||
'enableLocalSearchIndex': instance.enableLocalSearchIndex,
|
'enableLocalSearchIndex': instance.enableLocalSearchIndex,
|
||||||
'indexPrivateTabs': instance.indexPrivateTabs,
|
'indexPrivateTabs': instance.indexPrivateTabs,
|
||||||
'acceptSuggestionOnSubmit': instance.acceptSuggestionOnSubmit,
|
'acceptSuggestionOnSubmit': instance.acceptSuggestionOnSubmit,
|
||||||
|
'pureBlack': instance.pureBlack,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$ThemeModeEnumMap = {
|
const _$ThemeModeEnumMap = {
|
||||||
|
|||||||
@@ -265,6 +265,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
|
|||||||
DriftSqlType.bool,
|
DriftSqlType.bool,
|
||||||
db.typeMapping,
|
db.typeMapping,
|
||||||
),
|
),
|
||||||
|
'pureBlack': settings['pureBlack']?.readAs(
|
||||||
|
DriftSqlType.bool,
|
||||||
|
db.typeMapping,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$generalSettingsRepositoryHash() =>
|
String _$generalSettingsRepositoryHash() =>
|
||||||
r'1e7c7955669933a2dac5a279bcfbac54246672ca';
|
r'5036124c086e35c0e4616a0fd3b2276adb66dd5f';
|
||||||
|
|
||||||
abstract class _$GeneralSettingsRepository
|
abstract class _$GeneralSettingsRepository
|
||||||
extends $StreamNotifier<GeneralSettings> {
|
extends $StreamNotifier<GeneralSettings> {
|
||||||
|
|||||||
@@ -84,6 +84,28 @@ ColorScheme _fixSurfaceContainerColors(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rewrites a dark [ColorScheme] to use pure-black ("OLED"/high-contrast)
|
||||||
|
/// surfaces.
|
||||||
|
///
|
||||||
|
/// Only the *base* tones (the scaffold/page background and the lowest
|
||||||
|
/// containers) become true black for the power saving. The elevated container
|
||||||
|
/// tones keep meaningful grey steps so cards, sheets and menus stay visibly
|
||||||
|
/// separated from the black background — Material elevation shadows are
|
||||||
|
/// invisible on black, so the surface-tint step is the only separation cue and
|
||||||
|
/// it must stay perceptible. Steps below ~`#12` are imperceptible near black, so
|
||||||
|
/// the elevated tones climb in larger increments than the default dark scheme.
|
||||||
|
ColorScheme _applyPureBlackSurfaces(ColorScheme scheme) {
|
||||||
|
return scheme.copyWith(
|
||||||
|
surface: const Color(0xFF000000),
|
||||||
|
surfaceDim: const Color(0xFF000000),
|
||||||
|
surfaceContainerLowest: const Color(0xFF000000),
|
||||||
|
surfaceContainerLow: const Color(0xFF121212),
|
||||||
|
surfaceContainer: const Color(0xFF1B1B1B),
|
||||||
|
surfaceContainerHigh: const Color(0xFF242424),
|
||||||
|
surfaceContainerHighest: const Color(0xFF2E2E2E),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool _hasBrokenSurfaceContainerColors(ColorScheme scheme) {
|
bool _hasBrokenSurfaceContainerColors(ColorScheme scheme) {
|
||||||
return scheme.surfaceContainerLowest == scheme.surface &&
|
return scheme.surfaceContainerLowest == scheme.surface &&
|
||||||
scheme.surfaceContainerLow == scheme.surface &&
|
scheme.surfaceContainerLow == scheme.surface &&
|
||||||
@@ -187,6 +209,9 @@ class _MainWidget extends HookConsumerWidget {
|
|||||||
(value) => value.showModalBarrier,
|
(value) => value.showModalBarrier,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final pureBlack = ref.watch(
|
||||||
|
generalSettingsWithDefaultsProvider.select((value) => value.pureBlack),
|
||||||
|
);
|
||||||
|
|
||||||
useOnInitialization(() async {
|
useOnInitialization(() async {
|
||||||
await CountryCodes.init();
|
await CountryCodes.init();
|
||||||
@@ -361,6 +386,10 @@ class _MainWidget extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pureBlack) {
|
||||||
|
darkColorScheme = _applyPureBlackSurfaces(darkColorScheme);
|
||||||
|
}
|
||||||
|
|
||||||
return MainApp(
|
return MainApp(
|
||||||
key: rootKey,
|
key: rootKey,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
@@ -389,7 +418,9 @@ class _MainWidget extends HookConsumerWidget {
|
|||||||
bottomSheetTheme: BottomSheetThemeData(
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
modalBarrierColor: showModalBarrier ? null : Colors.transparent,
|
modalBarrierColor: showModalBarrier ? null : Colors.transparent,
|
||||||
),
|
),
|
||||||
extensions: const <ThemeExtension<dynamic>>[AppColors.dark],
|
extensions: <ThemeExtension<dynamic>>[
|
||||||
|
if (pureBlack) AppColors.darkOled else AppColors.dark,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
uiScaleFactor: uiScaleFactor,
|
uiScaleFactor: uiScaleFactor,
|
||||||
|
|||||||
Reference in New Issue
Block a user