feat: add free dark mode (#1748)
This commit is contained in:
@@ -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<Props> = ({ application, mainApplicatio
|
||||
|
||||
return (
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<DarkModeHandler application={application} />
|
||||
<ResponsivePaneProvider>
|
||||
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
|
||||
<div className={platformString + ' main-ui-view sn-component'}>
|
||||
|
||||
@@ -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
|
||||
@@ -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<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]),
|
||||
@@ -63,6 +58,11 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
}
|
||||
})
|
||||
|
||||
themesAsItems.unshift({
|
||||
label: 'Dark',
|
||||
value: 'Dark',
|
||||
})
|
||||
|
||||
themesAsItems.unshift({
|
||||
label: 'Default',
|
||||
value: 'Default',
|
||||
|
||||
@@ -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<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)
|
||||
@@ -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(() => {
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -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<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`)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user