appearance.dart 14 KB

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