Skip to content

Commit

Permalink
feat: add free dark mode (#1748)
Browse files Browse the repository at this point in the history
  • Loading branch information
amanharwara committed Oct 5, 2022
1 parent 6c26b96 commit 09b994f
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 27 deletions.
6 changes: 4 additions & 2 deletions packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export enum PrefKey {
NewNoteTitleFormat = 'newNoteTitleFormat',
CustomNoteTitleFormat = 'customNoteTitleFormat',
UpdateSavingStatusIndicator = 'updateSavingStatusIndicator',
DarkMode = 'darkMode',
}

export enum NewNoteTitleFormat {
Expand Down Expand Up @@ -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
Expand All @@ -99,4 +100,5 @@ export type PrefValue = {
[PrefKey.EditorLineHeight]: EditorLineHeight
[PrefKey.EditorFontSize]: EditorFontSize
[PrefKey.UpdateSavingStatusIndicator]: boolean
[PrefKey.DarkMode]: boolean
}
6 changes: 3 additions & 3 deletions packages/styles/src/Styles/_colors.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
38 changes: 31 additions & 7 deletions packages/ui-services/src/Theme/ThemeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = []
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -190,6 +191,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio

return (
<AndroidBackHandlerProvider application={application}>
<DarkModeHandler application={application} />
<ResponsivePaneProvider>
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
<div className={platformString + ' main-ui-view sn-component'}>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -21,18 +21,13 @@ type Props = {

const Appearance: FunctionComponent<Props> = ({ application }) => {
const premiumModal = usePremiumModal()
const isEntitledToMidnightTheme =
application.features.getFeatureStatus(FeatureIdentifier.MidnightTheme) === FeatureStatus.Entitled

const [themeItems, setThemeItems] = useState<DropdownItem[]>([])
const [autoLightTheme, setAutoLightTheme] = useState<string>(() =>
application.getPreference(PrefKey.AutoLightThemeIdentifier, PrefDefaults[PrefKey.AutoLightThemeIdentifier]),
)
const [autoDarkTheme, setAutoDarkTheme] = useState<string>(() =>
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]),
Expand Down Expand Up @@ -63,6 +58,11 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
}
})

themesAsItems.unshift({
label: 'Dark',
value: 'Dark',
})

themesAsItems.unshift({
label: 'Default',
value: 'Default',
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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

Expand All @@ -38,7 +47,19 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
const { closeQuickSettingsMenu, focusModeEnabled, setFocusModeEnabled } = quickSettingsMenuController
const [themes, setThemes] = useState<ThemeItem[]>([])
const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([])
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<HTMLButtonElement>(null)
const defaultThemeButtonRef = useRef<HTMLButtonElement>(null)
Expand Down Expand Up @@ -73,8 +94,6 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
})

setThemes(themes.sort(sortThemes))

setDefaultThemeOn(!themes.map((item) => item?.component).find((theme) => theme?.active && !theme.isLayerable()))
}, [application])

const reloadToggleableComponents = useCallback(() => {
Expand Down Expand Up @@ -131,13 +150,25 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ 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 (
<div ref={mainRef}>
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Themes</div>
Expand All @@ -149,6 +180,13 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
<RadioIndicator checked={defaultThemeOn} className="mr-2" />
Default
</button>
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={toggleDarkMode}
>
<RadioIndicator checked={isDarkModeOn} className="mr-2" />
Dark
</button>
{themes.map((theme) => (
<ThemesMenuButton item={theme} application={application} key={theme.component?.uuid ?? theme.identifier} />
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -32,10 +32,15 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ 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`)
Expand Down
3 changes: 2 additions & 1 deletion packages/web/src/javascripts/Constants/PrefDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 09b994f

Please sign in to comment.