appearance.dart 17 KB

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