appearance.dart 14 KB

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