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ó"
},
"appearance": {
"lightLabel": "Mode Clar",
"darkLabel": "Mode Fosc"
"themeMode": {
"label": "Theme Mode",
"light": "Mode Clar",
"dark": "Mode Fosc",
"system": "Adapt to System"
}
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
: _setting = setting,
super(AppearanceSettingsState.initial(
setting.theme,
setting.themeMode,
setting.font,
setting.monospaceFont,
setting.locale,
@ -26,21 +27,34 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
/// 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 setTheme(Brightness brightness) {
if (state.theme.brightness == brightness) {
// void setTheme(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;
}
_setting.theme = themeTypeToString(brightness);
_setting.themeMode = _themeModeToPB(themeMode);
_saveAppearanceSettings();
emit(state.copyWith(
theme: AppTheme.fromName(
themeName: _setting.theme,
font: state.theme.font,
monospaceFont: state.theme.monospaceFont,
),
));
emit(state.copyWith(themeMode: themeMode));
}
/// 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
class AppearanceSettingsState with _$AppearanceSettingsState {
const factory AppearanceSettingsState({
required AppTheme theme,
required AppTheme darkTheme,
required ThemeMode themeMode,
required Locale locale,
}) = _AppearanceSettingsState;
factory AppearanceSettingsState.initial(
String themeName,
ThemeModePB themeMode,
String font,
String monospaceFont,
LocaleSettingsPB locale,
) =>
AppearanceSettingsState(
theme: AppTheme.fromName(
themeName: themeName,
theme: AppTheme.fromBrightness(
brightness: Brightness.light,
font: font,
monospaceFont: monospaceFont,
),
darkTheme: AppTheme.fromBrightness(
brightness: Brightness.dark,
font: font,
monospaceFont: monospaceFont,
),
themeMode: _themeModeFromPB(themeMode),
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/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: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_bloc/flutter_bloc.dart';
import '../../widgets/toggle/toggle.dart';
class SettingsAppearanceView extends StatelessWidget {
const SettingsAppearanceView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.medium(LocaleKeys.settings_appearance_lightLabel.tr()),
Toggle(
value: Theme.of(context).brightness == Brightness.dark,
onChanged: (_) => setTheme(context),
style: ToggleStyle.big,
),
FlowyText.medium(LocaleKeys.settings_appearance_darkLabel.tr())
ThemeModeSetting(currentThemeMode: state.themeMode),
],
);
},
),
);
}
}
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) {
if (Theme.of(context).brightness == Brightness.dark) {
context.read<AppearanceSettingsCubit>().setTheme(Brightness.light);
} else {
context.read<AppearanceSettingsCubit>().setTheme(Brightness.dark);
String _themeModeLabelText(ThemeMode themeMode) {
switch (themeMode) {
case (ThemeMode.light):
return LocaleKeys.settings_appearance_themeMode_light.tr();
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
AppTheme({this.brightness = Brightness.light});
factory AppTheme.fromName({
required String themeName,
factory AppTheme.fromBrightness({
required Brightness brightness,
required String font,
required String monospaceFont,
}) {
switch (themeTypeFromString(themeName)) {
switch (brightness) {
case Brightness.light:
return AppTheme(brightness: Brightness.light)
..surface = Colors.white

View File

@ -63,7 +63,9 @@ class FlowyButton extends StatelessWidget {
children.add(Expanded(child: text));
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(

View File

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