appearance.dart 18 KB

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