appearance.dart 11 KB

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