appearance.dart 13 KB

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