Bläddra i källkod

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
Richard Shiue 2 år sedan
förälder
incheckning
442dfe7ef8

+ 7 - 3
frontend/app_flowy/assets/translations/ca-ES.json

@@ -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"
   }
-}
+}

+ 6 - 2
frontend/app_flowy/assets/translations/en.json

@@ -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": {

+ 8 - 4
frontend/app_flowy/assets/translations/es-VE.json

@@ -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"
     }
   }
-}
+}

+ 7 - 3
frontend/app_flowy/assets/translations/fr-CA.json

@@ -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"
   }
-}
+}

+ 6 - 2
frontend/app_flowy/assets/translations/fr-FR.json

@@ -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": {

+ 7 - 3
frontend/app_flowy/assets/translations/hu-HU.json

@@ -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"
   }
-}
+}

+ 6 - 2
frontend/app_flowy/assets/translations/id-ID.json

@@ -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": {

+ 10 - 6
frontend/app_flowy/assets/translations/it-IT.json

@@ -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"
   }
-}
+}

+ 6 - 2
frontend/app_flowy/assets/translations/ja-JP.json

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

+ 7 - 3
frontend/app_flowy/assets/translations/pl-PL.json

@@ -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"
   }
-}
+}

+ 6 - 2
frontend/app_flowy/assets/translations/pt-BR.json

@@ -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": {

+ 6 - 2
frontend/app_flowy/assets/translations/ru-RU.json

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

+ 7 - 3
frontend/app_flowy/assets/translations/sv.json

@@ -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"
     }
   }
-}
+}

+ 7 - 3
frontend/app_flowy/assets/translations/tr-TR.json

@@ -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"
   }
-}
+}

+ 6 - 2
frontend/app_flowy/assets/translations/zh-CN.json

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

+ 6 - 2
frontend/app_flowy/assets/translations/zh-TW.json

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

+ 2 - 0
frontend/app_flowy/lib/startup/tasks/app_widget.dart

@@ -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,

+ 59 - 12
frontend/app_flowy/lib/workspace/application/appearance.dart

@@ -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),
       );
 }

+ 77 - 21
frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart

@@ -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 "";
     }
   }
 }

+ 3 - 3
frontend/app_flowy/packages/flowy_infra/lib/theme.dart

@@ -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

+ 3 - 1
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -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(

+ 23 - 5
frontend/rust-lib/flowy-user/src/entities/user_setting.rs

@@ -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(),