appearance.dart 12 KB

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