mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
f81d5eb23e
commit
442dfe7ef8
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -138,8 +138,12 @@
|
||||
"open": "設定"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "ライトモード",
|
||||
"darkLabel": "ダークモード"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "ライトモード",
|
||||
"dark": "ダークモード",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
@ -151,8 +151,12 @@
|
||||
"open": "Открыть настройки"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Светлая",
|
||||
"darkLabel": "Тёмная"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Светлая",
|
||||
"dark": "Тёмная",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -152,8 +152,12 @@
|
||||
"open": "打开设置"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "日间模式",
|
||||
"darkLabel": "夜间模式"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "日间模式",
|
||||
"dark": "夜间模式",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
@ -145,8 +145,12 @@
|
||||
"open": "開啟設定"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "亮色模式",
|
||||
"darkLabel": "暗色模式"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "亮色模式",
|
||||
"dark": "暗色模式",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user