feat: switch between light and dark theme based on system settings (#1523)

* feat: allow listening to system for light/dark theme

* chore: implement UI for theme mode setting

* chore: fix translations
This commit is contained in:
Richard Shiue 2022-12-08 14:21:11 +08:00 committed by GitHub
parent f81d5eb23e
commit 442dfe7ef8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 275 additions and 86 deletions

View File

@ -138,12 +138,16 @@
"open": "Obrir la configuració" "open": "Obrir la configuració"
}, },
"appearance": { "appearance": {
"lightLabel": "Mode Clar", "themeMode": {
"darkLabel": "Mode Fosc" "label": "Theme Mode",
"light": "Mode Clar",
"dark": "Mode Fosc",
"system": "Adapt to System"
}
} }
}, },
"sideBar": { "sideBar": {
"openSidebar": "Open sidebar", "openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar" "closeSidebar": "Close sidebar"
} }
} }

View File

@ -158,8 +158,12 @@
"open": "Open Settings" "open": "Open Settings"
}, },
"appearance": { "appearance": {
"lightLabel": "Light Mode", "themeMode": {
"darkLabel": "Dark Mode" "label": "Theme Mode",
"light": "Light Mode",
"dark": "Dark Mode",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {

View File

@ -144,8 +144,12 @@
"open": "Abrir ajustes" "open": "Abrir ajustes"
}, },
"appearance": { "appearance": {
"lightLabel": "Modo Claro", "themeMode": {
"darkLabel": "Modo Oscuro" "label": "Theme Mode",
"light": "Modo Claro",
"dark": "Modo Oscuro",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {
@ -218,9 +222,9 @@
"openSidebar": "Abrir panel lateral", "openSidebar": "Abrir panel lateral",
"closeSidebar": "Cerrar panel lateral" "closeSidebar": "Cerrar panel lateral"
}, },
"board": { "board": {
"column": { "column": {
"create_new_card": "Nuevo" "create_new_card": "Nuevo"
} }
} }
} }

View File

@ -138,12 +138,16 @@
"open": "Ouvrir les paramètres" "open": "Ouvrir les paramètres"
}, },
"appearance": { "appearance": {
"lightLabel": "Mode clair", "themeMode": {
"darkLabel": "Mode sombre" "label": "Theme Mode",
"light": "Mode clair",
"dark": "Mode sombre",
"system": "Adapt to System"
}
} }
}, },
"sideBar": { "sideBar": {
"openSidebar": "Open sidebar", "openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar" "closeSidebar": "Close sidebar"
} }
} }

View File

@ -152,8 +152,12 @@
"open": "Ouvrir les paramètres" "open": "Ouvrir les paramètres"
}, },
"appearance": { "appearance": {
"lightLabel": "Mode clair", "themeMode": {
"darkLabel": "Mode sombre" "label": "Theme Mode",
"light": "Mode clair",
"dark": "Mode sombre",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {

View File

@ -138,12 +138,16 @@
"open": "Beállítások megnyitása" "open": "Beállítások megnyitása"
}, },
"appearance": { "appearance": {
"lightLabel": "Világos mód", "themeMode": {
"darkLabel": "Éjjeli mód" "label": "Theme Mode",
"light": "Világos mód",
"dark": "Éjjeli mód",
"system": "Adapt to System"
}
} }
}, },
"sideBar": { "sideBar": {
"openSidebar": "Open sidebar", "openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar" "closeSidebar": "Close sidebar"
} }
} }

View File

@ -145,8 +145,12 @@
"open": "Buka Pengaturan" "open": "Buka Pengaturan"
}, },
"appearance": { "appearance": {
"lightLabel": "Mode Terang", "themeMode": {
"darkLabel": "Mode Gelap" "label": "Theme Mode",
"light": "Mode Terang",
"dark": "Mode Gelap",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {

View File

@ -138,18 +138,22 @@
"open": "aprire le impostazioni" "open": "aprire le impostazioni"
}, },
"appearance": { "appearance": {
"lightLabel": "Modalità Chiara", "themeMode": {
"darkLabel": "Modalità Scura" "label": "Theme Mode",
"light": "Modalità Chiara",
"dark": "Modalità Scura",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {
"menuName":"Griglia" "menuName": "Griglia"
}, },
"document":{ "document": {
"menuName":"Documento" "menuName": "Documento"
}, },
"sideBar": { "sideBar": {
"openSidebar": "Open sidebar", "openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar" "closeSidebar": "Close sidebar"
} }
} }

View File

@ -138,8 +138,12 @@
"open": "設定" "open": "設定"
}, },
"appearance": { "appearance": {
"lightLabel": "ライトモード", "themeMode": {
"darkLabel": "ダークモード" "label": "Theme Mode",
"light": "ライトモード",
"dark": "ダークモード",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {

View File

@ -138,12 +138,16 @@
"open": "Otwórz Ustawienia" "open": "Otwórz Ustawienia"
}, },
"appearance": { "appearance": {
"lightLabel": "Tryb Jasny", "themeMode": {
"darkLabel": "Tryb Ciemny" "label": "Theme Mode",
"light": "Tryb Jasny",
"dark": "Tryb Ciemny",
"system": "Adapt to System"
}
} }
}, },
"sideBar": { "sideBar": {
"openSidebar": "Open sidebar", "openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar" "closeSidebar": "Close sidebar"
} }
} }

View File

@ -152,8 +152,12 @@
"open": "Abrir as Configurações" "open": "Abrir as Configurações"
}, },
"appearance": { "appearance": {
"lightLabel": "Modo Claro", "themeMode": {
"darkLabel": "Modo Escuro" "label": "Theme Mode",
"light": "Modo Claro",
"dark": "Modo Escuro",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {

View File

@ -151,8 +151,12 @@
"open": "Открыть настройки" "open": "Открыть настройки"
}, },
"appearance": { "appearance": {
"lightLabel": "Светлая", "themeMode": {
"darkLabel": "Тёмная" "label": "Theme Mode",
"light": "Светлая",
"dark": "Тёмная",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {

View File

@ -152,8 +152,12 @@
"open": "Öppna inställningarna" "open": "Öppna inställningarna"
}, },
"appearance": { "appearance": {
"lightLabel": "Ljust läge", "themeMode": {
"darkLabel": "Mörkt läge" "label": "Theme Mode",
"light": "Ljust läge",
"dark": "Mörkt läge",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {
@ -232,4 +236,4 @@
"create_new_card": "Nytt" "create_new_card": "Nytt"
} }
} }
} }

View File

@ -138,12 +138,16 @@
"open": "Ayarları Aç" "open": "Ayarları Aç"
}, },
"appearance": { "appearance": {
"lightLabel": "Aydınlık Mod", "themeMode": {
"darkLabel": "Karanlık Mod" "label": "Theme Mode",
"light": "Aydınlık Mod",
"dark": "Karanlık Mod",
"system": "Adapt to System"
}
} }
}, },
"sideBar": { "sideBar": {
"openSidebar": "Open sidebar", "openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar" "closeSidebar": "Close sidebar"
} }
} }

View File

@ -152,8 +152,12 @@
"open": "打开设置" "open": "打开设置"
}, },
"appearance": { "appearance": {
"lightLabel": "日间模式", "themeMode": {
"darkLabel": "夜间模式" "label": "Theme Mode",
"light": "日间模式",
"dark": "夜间模式",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {

View File

@ -145,8 +145,12 @@
"open": "開啟設定" "open": "開啟設定"
}, },
"appearance": { "appearance": {
"lightLabel": "亮色模式", "themeMode": {
"darkLabel": "暗色模式" "label": "Theme Mode",
"light": "亮色模式",
"dark": "暗色模式",
"system": "Adapt to System"
}
} }
}, },
"grid": { "grid": {

View File

@ -83,6 +83,8 @@ class ApplicationWidget extends StatelessWidget {
builder: overlayManagerBuilder(), builder: overlayManagerBuilder(),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: state.theme.getThemeData(state.locale), theme: state.theme.getThemeData(state.locale),
darkTheme: state.darkTheme.getThemeData(state.locale),
themeMode: state.themeMode,
localizationsDelegates: context.localizationDelegates + localizationsDelegates: context.localizationDelegates +
[AppFlowyEditorLocalizations.delegate], [AppFlowyEditorLocalizations.delegate],
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,

View File

@ -19,6 +19,7 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
: _setting = setting, : _setting = setting,
super(AppearanceSettingsState.initial( super(AppearanceSettingsState.initial(
setting.theme, setting.theme,
setting.themeMode,
setting.font, setting.font,
setting.monospaceFont, setting.monospaceFont,
setting.locale, setting.locale,
@ -26,21 +27,34 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
/// Updates the current theme and notify the listeners the theme was changed. /// Updates the current theme and notify the listeners the theme was changed.
/// Do nothing if the passed in themeType equal to the current theme type. /// Do nothing if the passed in themeType equal to the current theme type.
void setTheme(Brightness brightness) { // void setTheme(Brightness brightness) {
if (state.theme.brightness == brightness) { // if (state.theme.brightness == brightness) {
// return;
// }
// _setting.theme = themeTypeToString(brightness);
// _saveAppearanceSettings();
// emit(state.copyWith(
// theme: AppTheme.fromBrightness(
// brightness: _setting.themeMode,
// font: state.theme.font,
// monospaceFont: state.theme.monospaceFont,
// ),
// ));
// }
/// Updates the current theme and notify the listeners the theme was changed.
/// Do nothing if the passed in themeType equal to the current theme type.
void setThemeMode(ThemeMode themeMode) {
if (state.themeMode == themeMode) {
return; return;
} }
_setting.theme = themeTypeToString(brightness); _setting.themeMode = _themeModeToPB(themeMode);
_saveAppearanceSettings(); _saveAppearanceSettings();
emit(state.copyWith( emit(state.copyWith(themeMode: themeMode));
theme: AppTheme.fromName(
themeName: _setting.theme,
font: state.theme.font,
monospaceFont: state.theme.monospaceFont,
),
));
} }
/// Updates the current locale and notify the listeners the locale was changed /// Updates the current locale and notify the listeners the locale was changed
@ -115,25 +129,58 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
} }
} }
ThemeMode _themeModeFromPB(ThemeModePB themeModePB) {
switch (themeModePB) {
case ThemeModePB.Light:
return ThemeMode.light;
case ThemeModePB.Dark:
return ThemeMode.dark;
case ThemeModePB.System:
default:
return ThemeMode.system;
}
}
ThemeModePB _themeModeToPB(ThemeMode themeMode) {
switch (themeMode) {
case ThemeMode.light:
return ThemeModePB.Light;
case ThemeMode.dark:
return ThemeModePB.Dark;
case ThemeMode.system:
default:
return ThemeModePB.System;
}
}
@freezed @freezed
class AppearanceSettingsState with _$AppearanceSettingsState { class AppearanceSettingsState with _$AppearanceSettingsState {
const factory AppearanceSettingsState({ const factory AppearanceSettingsState({
required AppTheme theme, required AppTheme theme,
required AppTheme darkTheme,
required ThemeMode themeMode,
required Locale locale, required Locale locale,
}) = _AppearanceSettingsState; }) = _AppearanceSettingsState;
factory AppearanceSettingsState.initial( factory AppearanceSettingsState.initial(
String themeName, String themeName,
ThemeModePB themeMode,
String font, String font,
String monospaceFont, String monospaceFont,
LocaleSettingsPB locale, LocaleSettingsPB locale,
) => ) =>
AppearanceSettingsState( AppearanceSettingsState(
theme: AppTheme.fromName( theme: AppTheme.fromBrightness(
themeName: themeName, brightness: Brightness.light,
font: font, font: font,
monospaceFont: monospaceFont, monospaceFont: monospaceFont,
), ),
darkTheme: AppTheme.fromBrightness(
brightness: Brightness.dark,
font: font,
monospaceFont: monospaceFont,
),
themeMode: _themeModeFromPB(themeMode),
locale: Locale(locale.languageCode, locale.countryCode), locale: Locale(locale.languageCode, locale.countryCode),
); );
} }

View File

@ -1,43 +1,99 @@
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/workspace/application/appearance.dart'; import 'package:app_flowy/workspace/application/appearance.dart';
import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../widgets/toggle/toggle.dart';
class SettingsAppearanceView extends StatelessWidget { class SettingsAppearanceView extends StatelessWidget {
const SettingsAppearanceView({Key? key}) : super(key: key); const SettingsAppearanceView({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
crossAxisAlignment: CrossAxisAlignment.start, builder: (context, state) {
children: [ return Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
FlowyText.medium(LocaleKeys.settings_appearance_lightLabel.tr()), ThemeModeSetting(currentThemeMode: state.themeMode),
Toggle(
value: Theme.of(context).brightness == Brightness.dark,
onChanged: (_) => setTheme(context),
style: ToggleStyle.big,
),
FlowyText.medium(LocaleKeys.settings_appearance_darkLabel.tr())
], ],
);
},
),
);
}
}
class ThemeModeSetting extends StatelessWidget {
final ThemeMode currentThemeMode;
const ThemeModeSetting({required this.currentThemeMode, super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: FlowyText.medium(
LocaleKeys.settings_appearance_themeMode_label.tr(),
overflow: TextOverflow.ellipsis,
), ),
], ),
AppFlowyPopover(
direction: PopoverDirection.bottomWithRightAligned,
child: FlowyTextButton(
_themeModeLabelText(currentThemeMode),
fillColor: Colors.transparent,
hoverColor: Theme.of(context).colorScheme.secondary,
onPressed: () {},
),
popupBuilder: (BuildContext context) {
return IntrinsicWidth(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_themeModeItemButton(context, ThemeMode.light),
_themeModeItemButton(context, ThemeMode.dark),
_themeModeItemButton(context, ThemeMode.system),
],
),
);
},
),
],
);
}
Widget _themeModeItemButton(BuildContext context, ThemeMode themeMode) {
return SizedBox(
height: 32,
child: FlowyButton(
text: FlowyText.medium(_themeModeLabelText(themeMode)),
rightIcon: currentThemeMode == themeMode
? svgWidget("grid/checkmark")
: const SizedBox(),
onTap: () {
if (currentThemeMode != themeMode) {
context.read<AppearanceSettingsCubit>().setThemeMode(themeMode);
}
},
), ),
); );
} }
void setTheme(BuildContext context) { String _themeModeLabelText(ThemeMode themeMode) {
if (Theme.of(context).brightness == Brightness.dark) { switch (themeMode) {
context.read<AppearanceSettingsCubit>().setTheme(Brightness.light); case (ThemeMode.light):
} else { return LocaleKeys.settings_appearance_themeMode_light.tr();
context.read<AppearanceSettingsCubit>().setTheme(Brightness.dark); case (ThemeMode.dark):
return LocaleKeys.settings_appearance_themeMode_dark.tr();
case (ThemeMode.system):
return LocaleKeys.settings_appearance_themeMode_system.tr();
default:
return "";
} }
} }
} }

View File

@ -73,12 +73,12 @@ class AppTheme {
/// Default constructor /// Default constructor
AppTheme({this.brightness = Brightness.light}); AppTheme({this.brightness = Brightness.light});
factory AppTheme.fromName({ factory AppTheme.fromBrightness({
required String themeName, required Brightness brightness,
required String font, required String font,
required String monospaceFont, required String monospaceFont,
}) { }) {
switch (themeTypeFromString(themeName)) { switch (brightness) {
case Brightness.light: case Brightness.light:
return AppTheme(brightness: Brightness.light) return AppTheme(brightness: Brightness.light)
..surface = Colors.white ..surface = Colors.white

View File

@ -63,7 +63,9 @@ class FlowyButton extends StatelessWidget {
children.add(Expanded(child: text)); children.add(Expanded(child: text));
if (rightIcon != null) { if (rightIcon != null) {
children.add(rightIcon!); children.add(const HSpace(6));
children.add(
SizedBox.fromSize(size: const Size.square(16), child: rightIcon!));
} }
Widget child = Row( Widget child = Row(

View File

@ -1,4 +1,4 @@
use flowy_derive::ProtoBuf; use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@ -17,26 +17,43 @@ pub struct AppearanceSettingsPB {
pub theme: String, pub theme: String,
#[pb(index = 2)] #[pb(index = 2)]
pub font: String, #[serde(default)]
pub theme_mode: ThemeModePB,
#[pb(index = 3)] #[pb(index = 3)]
pub monospace_font: String, pub font: String,
#[pb(index = 4)] #[pb(index = 4)]
pub monospace_font: String,
#[pb(index = 5)]
#[serde(default)] #[serde(default)]
pub locale: LocaleSettingsPB, pub locale: LocaleSettingsPB,
#[pb(index = 5)] #[pb(index = 6)]
#[serde(default = "DEFAULT_RESET_VALUE")] #[serde(default = "DEFAULT_RESET_VALUE")]
pub reset_to_default: bool, pub reset_to_default: bool,
#[pb(index = 6)] #[pb(index = 7)]
#[serde(default)] #[serde(default)]
pub setting_key_value: HashMap<String, String>, pub setting_key_value: HashMap<String, String>,
} }
const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT; const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT;
#[derive(ProtoBuf_Enum, Serialize, Deserialize, Clone, Debug)]
pub enum ThemeModePB {
Light = 0,
Dark = 1,
System = 2,
}
impl std::default::Default for ThemeModePB {
fn default() -> Self {
ThemeModePB::System
}
}
#[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)] #[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)]
pub struct LocaleSettingsPB { pub struct LocaleSettingsPB {
#[pb(index = 1)] #[pb(index = 1)]
@ -64,6 +81,7 @@ impl std::default::Default for AppearanceSettingsPB {
fn default() -> Self { fn default() -> Self {
AppearanceSettingsPB { AppearanceSettingsPB {
theme: APPEARANCE_DEFAULT_THEME.to_owned(), theme: APPEARANCE_DEFAULT_THEME.to_owned(),
theme_mode: ThemeModePB::default(),
font: APPEARANCE_DEFAULT_FONT.to_owned(), font: APPEARANCE_DEFAULT_FONT.to_owned(),
monospace_font: APPEARANCE_DEFAULT_MONOSPACE_FONT.to_owned(), monospace_font: APPEARANCE_DEFAULT_MONOSPACE_FONT.to_owned(),
locale: LocaleSettingsPB::default(), locale: LocaleSettingsPB::default(),