appearance.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import 'dart:async';
  2. import 'package:appflowy/user/application/user_settings_service.dart';
  3. import 'package:appflowy/workspace/application/appearance_defaults.dart';
  4. import 'package:appflowy_backend/log.dart';
  5. import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
  6. import 'package:easy_localization/easy_localization.dart';
  7. import 'package:flowy_infra/size.dart';
  8. import 'package:flowy_infra/theme.dart';
  9. import 'package:flowy_infra/theme_extension.dart';
  10. import 'package:flutter/material.dart';
  11. import 'package:flutter_bloc/flutter_bloc.dart';
  12. import 'package:freezed_annotation/freezed_annotation.dart';
  13. import 'package:google_fonts/google_fonts.dart';
  14. part 'appearance.freezed.dart';
  15. const _white = Color(0xFFFFFFFF);
  16. /// [AppearanceSettingsCubit] is used to modify the appearance of AppFlowy.
  17. /// It includes the [AppTheme], [ThemeMode], [TextStyles] and [Locale].
  18. class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
  19. final AppearanceSettingsPB _setting;
  20. AppearanceSettingsCubit(AppearanceSettingsPB setting)
  21. : _setting = setting,
  22. super(
  23. AppearanceSettingsState.initial(
  24. setting.theme,
  25. setting.themeMode,
  26. setting.font,
  27. setting.monospaceFont,
  28. setting.locale,
  29. setting.isMenuCollapsed,
  30. setting.menuOffset,
  31. ),
  32. );
  33. /// Update selected theme in the user's settings and emit an updated state
  34. /// with the AppTheme named [themeName].
  35. Future<void> setTheme(String themeName) async {
  36. _setting.theme = themeName;
  37. _saveAppearanceSettings();
  38. emit(state.copyWith(appTheme: await AppTheme.fromName(themeName)));
  39. }
  40. /// Reset the current user selected theme back to the default
  41. Future<void> resetTheme() =>
  42. setTheme(DefaultAppearanceSettings.kDefaultThemeName);
  43. /// Update the theme mode in the user's settings and emit an updated state.
  44. void setThemeMode(ThemeMode themeMode) {
  45. _setting.themeMode = _themeModeToPB(themeMode);
  46. _saveAppearanceSettings();
  47. emit(state.copyWith(themeMode: themeMode));
  48. }
  49. /// Resets the current brightness setting
  50. void resetThemeMode() =>
  51. setThemeMode(DefaultAppearanceSettings.kDefaultThemeMode);
  52. /// Toggle the theme mode
  53. void toggleThemeMode() {
  54. final currentThemeMode = state.themeMode;
  55. setThemeMode(
  56. currentThemeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light,
  57. );
  58. }
  59. /// Update selected font in the user's settings and emit an updated state
  60. /// with the font name.
  61. void setFontFamily(String fontFamilyName) {
  62. _setting.font = fontFamilyName;
  63. _saveAppearanceSettings();
  64. emit(state.copyWith(font: fontFamilyName));
  65. }
  66. /// Resets the current font family for the user preferences
  67. void resetFontFamily() =>
  68. setFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
  69. /// Updates the current locale and notify the listeners the locale was
  70. /// changed. Fallback to [en] locale if [newLocale] is not supported.
  71. void setLocale(BuildContext context, Locale newLocale) {
  72. if (!context.supportedLocales.contains(newLocale)) {
  73. // Log.warn("Unsupported locale: $newLocale, Fallback to locale: en");
  74. newLocale = const Locale('en');
  75. }
  76. context.setLocale(newLocale).catchError((e) {
  77. Log.warn('Catch error in setLocale: $e}');
  78. });
  79. if (state.locale != newLocale) {
  80. _setting.locale.languageCode = newLocale.languageCode;
  81. _setting.locale.countryCode = newLocale.countryCode ?? "";
  82. _saveAppearanceSettings();
  83. emit(state.copyWith(locale: newLocale));
  84. }
  85. }
  86. // Saves the menus current visibility
  87. void saveIsMenuCollapsed(bool collapsed) {
  88. _setting.isMenuCollapsed = collapsed;
  89. _saveAppearanceSettings();
  90. }
  91. // Saves the current resize offset of the menu
  92. void saveMenuOffset(double offset) {
  93. _setting.menuOffset = offset;
  94. _saveAppearanceSettings();
  95. }
  96. /// Saves key/value setting to disk.
  97. /// Removes the key if the passed in value is null
  98. void setKeyValue(String key, String? value) {
  99. if (key.isEmpty) {
  100. Log.warn("The key should not be empty");
  101. return;
  102. }
  103. if (value == null) {
  104. _setting.settingKeyValue.remove(key);
  105. }
  106. if (_setting.settingKeyValue[key] != value) {
  107. if (value == null) {
  108. _setting.settingKeyValue.remove(key);
  109. } else {
  110. _setting.settingKeyValue[key] = value;
  111. }
  112. }
  113. _saveAppearanceSettings();
  114. }
  115. String? getValue(String key) {
  116. if (key.isEmpty) {
  117. Log.warn("The key should not be empty");
  118. return null;
  119. }
  120. return _setting.settingKeyValue[key];
  121. }
  122. /// Called when the application launches.
  123. /// Uses the device locale when the application is opened for the first time.
  124. void readLocaleWhenAppLaunch(BuildContext context) {
  125. if (_setting.resetToDefault) {
  126. _setting.resetToDefault = false;
  127. _saveAppearanceSettings();
  128. setLocale(context, context.deviceLocale);
  129. return;
  130. }
  131. setLocale(context, state.locale);
  132. }
  133. Future<void> _saveAppearanceSettings() async {
  134. UserSettingsBackendService().setAppearanceSetting(_setting).then((result) {
  135. result.fold(
  136. (l) => null,
  137. (error) => Log.error(error),
  138. );
  139. });
  140. }
  141. }
  142. ThemeMode _themeModeFromPB(ThemeModePB themeModePB) {
  143. switch (themeModePB) {
  144. case ThemeModePB.Light:
  145. return ThemeMode.light;
  146. case ThemeModePB.Dark:
  147. return ThemeMode.dark;
  148. case ThemeModePB.System:
  149. default:
  150. return ThemeMode.system;
  151. }
  152. }
  153. ThemeModePB _themeModeToPB(ThemeMode themeMode) {
  154. switch (themeMode) {
  155. case ThemeMode.light:
  156. return ThemeModePB.Light;
  157. case ThemeMode.dark:
  158. return ThemeModePB.Dark;
  159. case ThemeMode.system:
  160. default:
  161. return ThemeModePB.System;
  162. }
  163. }
  164. @freezed
  165. class AppearanceSettingsState with _$AppearanceSettingsState {
  166. const AppearanceSettingsState._();
  167. const factory AppearanceSettingsState({
  168. required AppTheme appTheme,
  169. required ThemeMode themeMode,
  170. required String font,
  171. required String monospaceFont,
  172. required Locale locale,
  173. required bool isMenuCollapsed,
  174. required double menuOffset,
  175. }) = _AppearanceSettingsState;
  176. factory AppearanceSettingsState.initial(
  177. String themeName,
  178. ThemeModePB themeModePB,
  179. String font,
  180. String monospaceFont,
  181. LocaleSettingsPB localePB,
  182. bool isMenuCollapsed,
  183. double menuOffset,
  184. ) {
  185. return AppearanceSettingsState(
  186. appTheme: AppTheme.fallback,
  187. font: font,
  188. monospaceFont: monospaceFont,
  189. themeMode: _themeModeFromPB(themeModePB),
  190. locale: Locale(localePB.languageCode, localePB.countryCode),
  191. isMenuCollapsed: isMenuCollapsed,
  192. menuOffset: menuOffset,
  193. );
  194. }
  195. ThemeData get lightTheme => _getThemeData(Brightness.light);
  196. ThemeData get darkTheme => _getThemeData(Brightness.dark);
  197. ThemeData _getThemeData(Brightness brightness) {
  198. // Poppins and SF Mono are not well supported in some languages, so use the
  199. // built-in font for the following languages.
  200. final useBuiltInFontLanguages = [
  201. const Locale('zh', 'CN'),
  202. const Locale('zh', 'TW'),
  203. ];
  204. String fontFamily = font;
  205. String monospaceFontFamily = monospaceFont;
  206. if (useBuiltInFontLanguages.contains(locale)) {
  207. fontFamily = '';
  208. monospaceFontFamily = '';
  209. }
  210. final theme = brightness == Brightness.light
  211. ? appTheme.lightTheme
  212. : appTheme.darkTheme;
  213. final colorScheme = ColorScheme(
  214. brightness: brightness,
  215. primary: theme.primary,
  216. onPrimary: theme.onPrimary,
  217. primaryContainer: theme.main2,
  218. onPrimaryContainer: _white,
  219. // page title hover color
  220. secondary: theme.hoverBG1,
  221. onSecondary: theme.shader1,
  222. // setting value hover color
  223. secondaryContainer: theme.selector,
  224. onSecondaryContainer: theme.topbarBg,
  225. tertiary: theme.shader7,
  226. // Editor: toolbarColor
  227. onTertiary: theme.toolbarColor,
  228. tertiaryContainer: theme.questionBubbleBG,
  229. background: theme.surface,
  230. onBackground: theme.text,
  231. surface: theme.surface,
  232. // text&icon color when it is hovered
  233. onSurface: theme.hoverFG,
  234. // grey hover color
  235. inverseSurface: theme.hoverBG3,
  236. onError: theme.shader7,
  237. error: theme.red,
  238. outline: theme.shader4,
  239. surfaceVariant: theme.sidebarBg,
  240. shadow: theme.shadow,
  241. );
  242. const Set<MaterialState> scrollbarInteractiveStates = <MaterialState>{
  243. MaterialState.pressed,
  244. MaterialState.hovered,
  245. MaterialState.dragged,
  246. };
  247. return ThemeData(
  248. brightness: brightness,
  249. dialogBackgroundColor: theme.surface,
  250. textTheme: _getTextTheme(fontFamily: fontFamily, fontColor: theme.text),
  251. textSelectionTheme: TextSelectionThemeData(
  252. cursorColor: theme.main2,
  253. selectionHandleColor: theme.main2,
  254. ),
  255. iconTheme: IconThemeData(color: theme.icon),
  256. tooltipTheme: TooltipThemeData(
  257. textStyle: _getFontStyle(
  258. fontFamily: fontFamily,
  259. fontSize: FontSizes.s11,
  260. fontWeight: FontWeight.w400,
  261. fontColor: theme.surface,
  262. ),
  263. ),
  264. scaffoldBackgroundColor: theme.surface,
  265. snackBarTheme: SnackBarThemeData(
  266. backgroundColor: colorScheme.primary,
  267. contentTextStyle: TextStyle(color: colorScheme.onSurface),
  268. ),
  269. scrollbarTheme: ScrollbarThemeData(
  270. thumbColor: MaterialStateProperty.resolveWith((states) {
  271. if (states.any(scrollbarInteractiveStates.contains)) {
  272. return theme.shader7;
  273. }
  274. return theme.shader5;
  275. }),
  276. thickness: MaterialStateProperty.resolveWith((states) {
  277. if (states.any(scrollbarInteractiveStates.contains)) {
  278. return 4;
  279. }
  280. return 3.0;
  281. }),
  282. crossAxisMargin: 0.0,
  283. mainAxisMargin: 6.0,
  284. radius: Corners.s10Radius,
  285. ),
  286. materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  287. //dropdown menu color
  288. canvasColor: theme.surface,
  289. dividerColor: theme.divider,
  290. hintColor: theme.hint,
  291. //action item hover color
  292. hoverColor: theme.hoverBG2,
  293. disabledColor: theme.shader4,
  294. highlightColor: theme.main1,
  295. indicatorColor: theme.main1,
  296. cardColor: theme.input,
  297. colorScheme: colorScheme,
  298. extensions: [
  299. AFThemeExtension(
  300. warning: theme.yellow,
  301. success: theme.green,
  302. tint1: theme.tint1,
  303. tint2: theme.tint2,
  304. tint3: theme.tint3,
  305. tint4: theme.tint4,
  306. tint5: theme.tint5,
  307. tint6: theme.tint6,
  308. tint7: theme.tint7,
  309. tint8: theme.tint8,
  310. tint9: theme.tint9,
  311. textColor: theme.text,
  312. greyHover: theme.hoverBG1,
  313. greySelect: theme.bg3,
  314. lightGreyHover: theme.hoverBG3,
  315. toggleOffFill: theme.shader5,
  316. progressBarBGColor: theme.progressBarBGColor,
  317. toggleButtonBGColor: theme.toggleButtonBGColor,
  318. code: _getFontStyle(
  319. fontFamily: monospaceFontFamily,
  320. fontColor: theme.shader3,
  321. ),
  322. callout: _getFontStyle(
  323. fontFamily: fontFamily,
  324. fontSize: FontSizes.s11,
  325. fontColor: theme.shader3,
  326. ),
  327. calloutBGColor: theme.hoverBG3,
  328. caption: _getFontStyle(
  329. fontFamily: fontFamily,
  330. fontSize: FontSizes.s11,
  331. fontWeight: FontWeight.w400,
  332. fontColor: theme.hint,
  333. ),
  334. )
  335. ],
  336. );
  337. }
  338. TextStyle _getFontStyle({
  339. required String fontFamily,
  340. double? fontSize,
  341. FontWeight? fontWeight,
  342. Color? fontColor,
  343. double? letterSpacing,
  344. double? lineHeight,
  345. }) {
  346. try {
  347. return GoogleFonts.getFont(
  348. fontFamily,
  349. fontSize: fontSize ?? FontSizes.s12,
  350. color: fontColor,
  351. fontWeight: fontWeight ?? FontWeight.w500,
  352. letterSpacing: (fontSize ?? FontSizes.s12) * (letterSpacing ?? 0.005),
  353. height: lineHeight,
  354. );
  355. } catch (e) {
  356. return TextStyle(
  357. fontFamily: fontFamily,
  358. fontSize: fontSize ?? FontSizes.s12,
  359. color: fontColor,
  360. fontWeight: fontWeight ?? FontWeight.w500,
  361. fontFamilyFallback: const ["Noto Color Emoji"],
  362. letterSpacing: (fontSize ?? FontSizes.s12) * (letterSpacing ?? 0.005),
  363. height: lineHeight,
  364. );
  365. }
  366. }
  367. TextTheme _getTextTheme({
  368. required String fontFamily,
  369. required Color fontColor,
  370. }) {
  371. return TextTheme(
  372. displayLarge: _getFontStyle(
  373. fontFamily: fontFamily,
  374. fontSize: FontSizes.s32,
  375. fontColor: fontColor,
  376. fontWeight: FontWeight.w600,
  377. lineHeight: 42.0,
  378. ), // h2
  379. displayMedium: _getFontStyle(
  380. fontFamily: fontFamily,
  381. fontSize: FontSizes.s24,
  382. fontColor: fontColor,
  383. fontWeight: FontWeight.w600,
  384. lineHeight: 34.0,
  385. ), // h3
  386. displaySmall: _getFontStyle(
  387. fontFamily: fontFamily,
  388. fontSize: FontSizes.s20,
  389. fontColor: fontColor,
  390. fontWeight: FontWeight.w600,
  391. lineHeight: 28.0,
  392. ), // h4
  393. titleLarge: _getFontStyle(
  394. fontFamily: fontFamily,
  395. fontSize: FontSizes.s18,
  396. fontColor: fontColor,
  397. fontWeight: FontWeight.w600,
  398. ), // title
  399. titleMedium: _getFontStyle(
  400. fontFamily: fontFamily,
  401. fontSize: FontSizes.s16,
  402. fontColor: fontColor,
  403. fontWeight: FontWeight.w600,
  404. ), // heading
  405. titleSmall: _getFontStyle(
  406. fontFamily: fontFamily,
  407. fontSize: FontSizes.s14,
  408. fontColor: fontColor,
  409. fontWeight: FontWeight.w600,
  410. ), // subheading
  411. bodyMedium: _getFontStyle(
  412. fontFamily: fontFamily,
  413. fontColor: fontColor,
  414. ), // body-regular
  415. bodySmall: _getFontStyle(
  416. fontFamily: fontFamily,
  417. fontColor: fontColor,
  418. fontWeight: FontWeight.w400,
  419. ), // body-thin
  420. );
  421. }
  422. }