From 09b994f8f938ae0536f42742f7a221df536c4c4a Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Thu, 6 Oct 2022 01:49:35 +0530 Subject: [PATCH] feat: add free dark mode (#1748) --- .../src/Domain/Syncable/UserPrefs/PrefKey.ts | 6 +- packages/styles/src/Styles/_colors.scss | 6 +- .../ui-services/src/Theme/ThemeManager.ts | 38 ++++++++-- .../ApplicationView/ApplicationView.tsx | 2 + .../DarkModeHandler/DarkModeHandler.tsx | 26 +++++++ .../Preferences/Panes/Appearance.tsx | 14 ++-- .../QuickSettingsMenu/QuickSettingsMenu.tsx | 48 +++++++++++-- .../QuickSettingsMenu/ThemesMenuButton.tsx | 9 ++- .../src/javascripts/Constants/PrefDefaults.ts | 3 +- packages/web/src/stylesheets/_dark-mode.scss | 71 +++++++++++++++++++ packages/web/src/stylesheets/index.css.scss | 1 + 11 files changed, 197 insertions(+), 27 deletions(-) create mode 100644 packages/web/src/javascripts/Components/DarkModeHandler/DarkModeHandler.tsx create mode 100644 packages/web/src/stylesheets/_dark-mode.scss diff --git a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts index be248a3ce..27f1d4287 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts @@ -37,6 +37,7 @@ export enum PrefKey { NewNoteTitleFormat = 'newNoteTitleFormat', CustomNoteTitleFormat = 'customNoteTitleFormat', UpdateSavingStatusIndicator = 'updateSavingStatusIndicator', + DarkMode = 'darkMode', } export enum NewNoteTitleFormat { @@ -82,8 +83,8 @@ export type PrefValue = { [PrefKey.NotesHideTags]: boolean [PrefKey.NotesHideEditorIcon]: boolean [PrefKey.UseSystemColorScheme]: boolean - [PrefKey.AutoLightThemeIdentifier]: FeatureIdentifier | 'Default' - [PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier | 'Default' + [PrefKey.AutoLightThemeIdentifier]: FeatureIdentifier | 'Default' | 'Dark' + [PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier | 'Default' | 'Dark' [PrefKey.NoteAddToParentFolders]: boolean [PrefKey.MobileSortNotesBy]: CollectionSortProperty [PrefKey.MobileSortNotesReverse]: boolean @@ -99,4 +100,5 @@ export type PrefValue = { [PrefKey.EditorLineHeight]: EditorLineHeight [PrefKey.EditorFontSize]: EditorFontSize [PrefKey.UpdateSavingStatusIndicator]: boolean + [PrefKey.DarkMode]: boolean } diff --git a/packages/styles/src/Styles/_colors.scss b/packages/styles/src/Styles/_colors.scss index 556de3c11..1ccdbf990 100644 --- a/packages/styles/src/Styles/_colors.scss +++ b/packages/styles/src/Styles/_colors.scss @@ -20,14 +20,14 @@ --sn-stylekit-background-color: #ffffff; // For borders inside background-color --sn-stylekit-border-color: #dfe1e4; - --sn-stylekit-foreground-color: #19191C; + --sn-stylekit-foreground-color: #19191c; // Colors for layers placed on top of non-prefixed background, border, and foreground - --sn-stylekit-contrast-background-color: rgba(244, 245, 247, 1.0); + --sn-stylekit-contrast-background-color: rgba(244, 245, 247, 1); --sn-stylekit-contrast-foreground-color: #2e2e2e; --sn-stylekit-contrast-border-color: #e3e3e3; // For borders inside contrast-background-color // Alternative set of background and contrast options - --sn-stylekit-secondary-background-color: #EEEFF1; + --sn-stylekit-secondary-background-color: #eeeff1; --sn-stylekit-secondary-foreground-color: #2e2e2e; --sn-stylekit-secondary-border-color: #e3e3e3; diff --git a/packages/ui-services/src/Theme/ThemeManager.ts b/packages/ui-services/src/Theme/ThemeManager.ts index caccee11e..dbc235bbb 100644 --- a/packages/ui-services/src/Theme/ThemeManager.ts +++ b/packages/ui-services/src/Theme/ThemeManager.ts @@ -20,6 +20,7 @@ import { const CachedThemesKey = 'cachedThemes' const TimeBeforeApplyingColorScheme = 5 const DefaultThemeIdentifier = 'Default' +const DarkThemeIdentifier = 'Dark' export class ThemeManager extends AbstractService { private activeThemes: Uuid[] = [] @@ -90,11 +91,13 @@ export class ThemeManager extends AbstractService { private handlePreferencesChangeEvent(): void { const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false) - if (useDeviceThemeSettings !== this.lastUseDeviceThemeSettings) { + const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings + + if (hasPreferenceChanged) { this.lastUseDeviceThemeSettings = useDeviceThemeSettings } - if (useDeviceThemeSettings) { + if (hasPreferenceChanged && useDeviceThemeSettings) { const prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)') this.setThemeAsPerColorScheme(prefersDarkColorScheme.matches) @@ -159,7 +162,11 @@ export class ThemeManager extends AbstractService { } private colorSchemeEventHandler(event: MediaQueryListEvent) { - this.setThemeAsPerColorScheme(event.matches) + const shouldChangeTheme = this.application.getPreference(PrefKey.UseSystemColorScheme, false) + + if (shouldChangeTheme) { + this.setThemeAsPerColorScheme(event.matches) + } } private showColorSchemeToast(setThemeCallback: () => void) { @@ -192,24 +199,41 @@ export class ThemeManager extends AbstractService { private setThemeAsPerColorScheme(prefersDarkColorScheme: boolean) { const preference = prefersDarkColorScheme ? PrefKey.AutoDarkThemeIdentifier : PrefKey.AutoLightThemeIdentifier + const preferenceDefault = + preference === PrefKey.AutoDarkThemeIdentifier ? DarkThemeIdentifier : DefaultThemeIdentifier const themes = this.application.items .getDisplayableComponents() .filter((component) => component.isTheme()) as SNTheme[] const activeTheme = themes.find((theme) => theme.active && !theme.isLayerable()) - const activeThemeIdentifier = activeTheme ? activeTheme.identifier : DefaultThemeIdentifier + const activeThemeIdentifier = activeTheme + ? activeTheme.identifier + : this.application.getPreference(PrefKey.DarkMode, false) + ? DarkThemeIdentifier + : DefaultThemeIdentifier - const themeIdentifier = this.application.getPreference(preference, DefaultThemeIdentifier) as string + const themeIdentifier = this.application.getPreference(preference, preferenceDefault) as string + + const toggleActiveTheme = () => { + if (activeTheme) { + void this.application.mutator.toggleTheme(activeTheme) + } + } const setTheme = () => { - if (themeIdentifier === DefaultThemeIdentifier && activeTheme) { - this.application.mutator.toggleTheme(activeTheme).catch(console.error) + if (themeIdentifier === DefaultThemeIdentifier) { + toggleActiveTheme() + void this.application.setPreference(PrefKey.DarkMode, false) + } else if (themeIdentifier === DarkThemeIdentifier) { + toggleActiveTheme() + void this.application.setPreference(PrefKey.DarkMode, true) } else { const theme = themes.find((theme) => theme.package_info.identifier === themeIdentifier) if (theme && !theme.active) { this.application.mutator.toggleTheme(theme).catch(console.error) } + void this.application.setPreference(PrefKey.DarkMode, false) } } diff --git a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx index b40db86b4..d89f2b4c9 100644 --- a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -27,6 +27,7 @@ import FileDragNDropProvider from '../FileDragNDropProvider/FileDragNDropProvide import ResponsivePaneProvider from '../ResponsivePane/ResponsivePaneProvider' import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler' import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal' +import DarkModeHandler from '../DarkModeHandler/DarkModeHandler' type Props = { application: WebApplication @@ -190,6 +191,7 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio return ( +
diff --git a/packages/web/src/javascripts/Components/DarkModeHandler/DarkModeHandler.tsx b/packages/web/src/javascripts/Components/DarkModeHandler/DarkModeHandler.tsx new file mode 100644 index 000000000..8b797088d --- /dev/null +++ b/packages/web/src/javascripts/Components/DarkModeHandler/DarkModeHandler.tsx @@ -0,0 +1,26 @@ +import { WebApplication } from '@/Application/Application' +import { PrefDefaults } from '@/Constants/PrefDefaults' +import { ApplicationEvent, PrefKey } from '@standardnotes/snjs' +import { useEffect } from 'react' + +type Props = { + application: WebApplication +} + +const DarkModeHandler = ({ application }: Props) => { + useEffect(() => { + application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => { + const isDarkModeOn = application.getPreference(PrefKey.DarkMode, PrefDefaults[PrefKey.DarkMode]) + + if (isDarkModeOn) { + document.documentElement.classList.add('dark-mode') + } else { + document.documentElement.classList.remove('dark-mode') + } + }) + }, [application]) + + return null +} + +export default DarkModeHandler diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx index f78cb853b..b9c84758a 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx @@ -4,7 +4,7 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal' import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import Switch from '@/Components/Switch/Switch' import { WebApplication } from '@/Application/Application' -import { ContentType, FeatureIdentifier, FeatureStatus, PrefKey, GetFeatures, SNTheme } from '@standardnotes/snjs' +import { ContentType, FeatureIdentifier, PrefKey, GetFeatures, SNTheme } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useEffect, useState } from 'react' import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content' @@ -21,18 +21,13 @@ type Props = { const Appearance: FunctionComponent = ({ application }) => { const premiumModal = usePremiumModal() - const isEntitledToMidnightTheme = - application.features.getFeatureStatus(FeatureIdentifier.MidnightTheme) === FeatureStatus.Entitled const [themeItems, setThemeItems] = useState([]) const [autoLightTheme, setAutoLightTheme] = useState(() => application.getPreference(PrefKey.AutoLightThemeIdentifier, PrefDefaults[PrefKey.AutoLightThemeIdentifier]), ) const [autoDarkTheme, setAutoDarkTheme] = useState(() => - application.getPreference( - PrefKey.AutoDarkThemeIdentifier, - isEntitledToMidnightTheme ? FeatureIdentifier.MidnightTheme : PrefDefaults[PrefKey.AutoDarkThemeIdentifier], - ), + application.getPreference(PrefKey.AutoDarkThemeIdentifier, PrefDefaults[PrefKey.AutoDarkThemeIdentifier]), ) const [useDeviceSettings, setUseDeviceSettings] = useState(() => application.getPreference(PrefKey.UseSystemColorScheme, PrefDefaults[PrefKey.UseSystemColorScheme]), @@ -63,6 +58,11 @@ const Appearance: FunctionComponent = ({ application }) => { } }) + themesAsItems.unshift({ + label: 'Dark', + value: 'Dark', + }) + themesAsItems.unshift({ label: 'Default', value: 'Default', diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx index be3440468..45a76a146 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -1,5 +1,13 @@ import { WebApplication } from '@/Application/Application' -import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent } from '@standardnotes/snjs' +import { + ApplicationEvent, + ComponentArea, + ContentType, + FeatureIdentifier, + GetFeatures, + PrefKey, + SNComponent, +} from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import Icon from '@/Components/Icon/Icon' @@ -12,6 +20,7 @@ import RadioIndicator from '../RadioIndicator/RadioIndicator' import HorizontalSeparator from '../Shared/HorizontalSeparator' import { QuickSettingsController } from '@/Controllers/QuickSettingsController' import PanelSettingsSection from './PanelSettingsSection' +import { PrefDefaults } from '@/Constants/PrefDefaults' const focusModeAnimationDuration = 1255 @@ -38,7 +47,19 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet const { closeQuickSettingsMenu, focusModeEnabled, setFocusModeEnabled } = quickSettingsMenuController const [themes, setThemes] = useState([]) const [toggleableComponents, setToggleableComponents] = useState([]) - const [defaultThemeOn, setDefaultThemeOn] = useState(false) + + const [isDarkModeOn, setDarkModeOn] = useState( + application.getPreference(PrefKey.DarkMode, PrefDefaults[PrefKey.DarkMode]), + ) + const defaultThemeOn = + !themes.map((item) => item?.component).find((theme) => theme?.active && !theme.isLayerable()) && !isDarkModeOn + + useEffect(() => { + application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => { + const isDarkModeOn = application.getPreference(PrefKey.DarkMode, PrefDefaults[PrefKey.DarkMode]) + setDarkModeOn(isDarkModeOn) + }) + }, [application]) const prefsButtonRef = useRef(null) const defaultThemeButtonRef = useRef(null) @@ -73,8 +94,6 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet }) setThemes(themes.sort(sortThemes)) - - setDefaultThemeOn(!themes.map((item) => item?.component).find((theme) => theme?.active && !theme.isLayerable())) }, [application]) const reloadToggleableComponents = useCallback(() => { @@ -131,13 +150,25 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet [application], ) - const toggleDefaultTheme = useCallback(() => { + const deactivateAnyNonLayerableTheme = useCallback(() => { const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable()) if (activeTheme) { application.mutator.toggleTheme(activeTheme).catch(console.error) } }, [application, themes]) + const toggleDefaultTheme = useCallback(() => { + deactivateAnyNonLayerableTheme() + application.setPreference(PrefKey.DarkMode, false) + }, [application, deactivateAnyNonLayerableTheme]) + + const toggleDarkMode = useCallback(() => { + if (!isDarkModeOn) { + deactivateAnyNonLayerableTheme() + application.setPreference(PrefKey.DarkMode, true) + } + }, [application, isDarkModeOn, deactivateAnyNonLayerableTheme]) + return (
Themes
@@ -149,6 +180,13 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet Default + {themes.map((theme) => ( ))} diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx index 71f60c186..11358f254 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx @@ -1,5 +1,5 @@ import { WebApplication } from '@/Application/Application' -import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs' +import { FeatureIdentifier, FeatureStatus, PrefKey } from '@standardnotes/snjs' import { FunctionComponent, MouseEventHandler, useCallback, useMemo } from 'react' import Icon from '@/Components/Icon/Icon' import { usePremiumModal } from '@/Hooks/usePremiumModal' @@ -32,10 +32,15 @@ const ThemesMenuButton: FunctionComponent = ({ application, item }) => { e.preventDefault() if (item.component && canActivateTheme) { - const themeIsLayerableOrNotActive = item.component.isLayerable() || !item.component.active + const isThemeLayerable = item.component.isLayerable() + const themeIsLayerableOrNotActive = isThemeLayerable || !item.component.active if (themeIsLayerableOrNotActive) { application.mutator.toggleTheme(item.component).catch(console.error) + + if (!isThemeLayerable) { + application.setPreference(PrefKey.DarkMode, false) + } } } else { premiumModal.activate(`${item.name} theme`) diff --git a/packages/web/src/javascripts/Constants/PrefDefaults.ts b/packages/web/src/javascripts/Constants/PrefDefaults.ts index cec9e5c4f..98cfcdd83 100644 --- a/packages/web/src/javascripts/Constants/PrefDefaults.ts +++ b/packages/web/src/javascripts/Constants/PrefDefaults.ts @@ -22,9 +22,10 @@ export const PrefDefaults = { [PrefKey.NotesHideEditorIcon]: false, [PrefKey.UseSystemColorScheme]: false, [PrefKey.AutoLightThemeIdentifier]: 'Default', - [PrefKey.AutoDarkThemeIdentifier]: 'Default', + [PrefKey.AutoDarkThemeIdentifier]: 'Dark', [PrefKey.NoteAddToParentFolders]: true, [PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat.CurrentDateAndTime, [PrefKey.CustomNoteTitleFormat]: 'YYYY-MM-DD [at] hh:mm A', [PrefKey.UpdateSavingStatusIndicator]: true, + [PrefKey.DarkMode]: false, } as const diff --git a/packages/web/src/stylesheets/_dark-mode.scss b/packages/web/src/stylesheets/_dark-mode.scss new file mode 100644 index 000000000..a12c7e155 --- /dev/null +++ b/packages/web/src/stylesheets/_dark-mode.scss @@ -0,0 +1,71 @@ +.dark-mode { + --foreground-color: #eeeeee; + --background-color: #0f1011; + --highlight-color: #a464c2; + --border-color: #0f1011; + + --sn-component-foreground-color: var(--foreground-color); + --sn-component-background-color: transparent; + --sn-component-foreground-highlight-color: var(--highlight-color); + --sn-component-outer-border-color: transparent; + --sn-component-inner-border-color: var(--foreground-color); + + // StyleKit Vars + + --sn-stylekit-passive-color-0: #999999; + --sn-stylekit-passive-color-3: #28292b; + --sn-stylekit-passive-color-4: #1c1d1e; + --sn-stylekit-passive-color-5: #1d1f20; + + --sn-stylekit-shadow-color: #000000; + + --sn-stylekit-info-color: var(--highlight-color); + --sn-stylekit-info-contrast-color: var(--foreground-color); + + --sn-stylekit-neutral-color: #7c7c7c; + --sn-stylekit-neutral-contrast-color: #ffffff; + + --sn-stylekit-success-color: #2b9612; + --sn-stylekit-success-contrast-color: #ffffff; + + --sn-stylekit-warning-color: #f6a200; + --sn-stylekit-warning-contrast-color: #ffffff; + + --sn-stylekit-danger-color: #f80324; + --sn-stylekit-danger-contrast-color: #ffffff; + + --sn-stylekit-editor-background-color: var(--sn-stylekit-background-color); + --sn-stylekit-editor-foreground-color: var(--sn-stylekit-foreground-color); + + --sn-stylekit-background-color: var(--background-color); + --sn-stylekit-foreground-color: var(--foreground-color); + --sn-stylekit-border-color: #000000; + + --sn-stylekit-contrast-background-color: #000000; + --sn-stylekit-contrast-foreground-color: #ffffff; + --sn-stylekit-contrast-border-color: #000000; + + --sn-stylekit-secondary-background-color: var(--sn-stylekit-passive-color-4); + --sn-stylekit-secondary-foreground-color: #ffffff; + --sn-stylekit-secondary-border-color: #000000; + + --sn-stylekit-secondary-contrast-background-color: #000000; + --sn-stylekit-secondary-contrast-foreground-color: #ffffff; + --sn-stylekit-secondary-contrast-border-color: #ffffff; + + --sn-stylekit-paragraph-text-color: #ffffff; + + --sn-desktop-titlebar-bg-color: var(--background-color); + --sn-desktop-titlebar-border-color: var(--border-color); + --sn-desktop-titlebar-ui-color: var(--foreground-color); + --sn-desktop-titlebar-ui-hover-color: var(--highlight-color); + + --sn-stylekit-scrollbar-track-border-color: var(--border-color); + --sn-stylekit-scrollbar-thumb-color: var(--sn-stylekit-info-color); + + --sn-stylekit-menu-border: 1px solid #424242; + + // Theme + + --navigation-item-selected-background-color: var(--background-color); +} diff --git a/packages/web/src/stylesheets/index.css.scss b/packages/web/src/stylesheets/index.css.scss index 8a814ced6..74be63612 100644 --- a/packages/web/src/stylesheets/index.css.scss +++ b/packages/web/src/stylesheets/index.css.scss @@ -3,6 +3,7 @@ @import '../../../styles/src/Styles/_scrollbar.scss'; @import '../../../styles/src/Styles/utils/_animation.scss'; @import 'theme'; +@import 'dark-mode'; @import 'main'; @import 'ui'; @import 'footer';