feat: reduce motion (#101)

* feat: init

Signed-off-by: Innei <i@innei.in>

* feat: reduce motion

Signed-off-by: Innei <i@innei.in>

* fix: image preview

Signed-off-by: Innei <i@innei.in>

* fix: guard ui setting storage

Signed-off-by: Innei <i@innei.in>

---------

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-07-01 17:26:48 +08:00 committed by GitHub
parent 16f9645758
commit 48e73d7e83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 219 additions and 137 deletions

View File

@ -1,2 +1,5 @@
export * from "./dom"
export * from "./route"
export * from "./sidebar"
export * from "./ui"
export * from "./user"

View File

@ -0,0 +1,75 @@
import { useRefValue } from "@renderer/hooks"
import { createAtomHooks } from "@renderer/lib/jotai"
import { getStorageNS } from "@renderer/lib/ns"
import { useAtomValue } from "jotai"
import { atomWithStorage, selectAtom } from "jotai/utils"
import { useMemo } from "react"
const createDefaultSettings = () => ({
// Sidebar
entryColWidth: 340,
opaqueSidebar: false,
sidebarShowUnreadCount: true,
// Global UI
uiTextSize: 16,
// System
showDockBadge: true,
// Misc
modalOverlay: true,
modalDraggable: true,
modalOpaque: true,
reduceMotion: false,
// Content
readerFontFamily: "SN Pro",
readerRenderInlineStyle: false,
codeHighlightTheme: "github-dark",
})
const atom = atomWithStorage(getStorageNS("ui"), createDefaultSettings())
const [, , useUISettingValue, , getUISettings, setUISettings] =
createAtomHooks(atom)
export const initializeDefaultUISettings = () => {
const currentSettings = getUISettings()
const defaultSettings = createDefaultSettings()
if (typeof currentSettings !== "object") setUISettings(defaultSettings)
const newSettings = { ...defaultSettings, ...currentSettings }
setUISettings(newSettings)
}
export { getUISettings, useUISettingValue }
export const useUISettingKey = <
T extends keyof ReturnType<typeof getUISettings>,
>(
key: T,
) => useAtomValue(useMemo(() => selectAtom(atom, (s) => s[key]), [key]))
export const useUISettingSelector = <
T extends keyof ReturnType<typeof getUISettings>,
S extends ReturnType<typeof getUISettings>,
R = S[T],
>(
selector: (s: S) => R,
): R => {
const stableSelector = useRefValue(selector)
return useAtomValue(
// @ts-expect-error
useMemo(() => selectAtom(atom, stableSelector.current), [stableSelector]),
)
}
export const setUISetting = <K extends keyof ReturnType<typeof getUISettings>>(
key: K,
value: ReturnType<typeof getUISettings>[K],
) => {
setUISettings({
...getUISettings(),
[key]: value,
})
}
export const clearUISettings = () => {
setUISettings(createDefaultSettings())
}

View File

@ -0,0 +1,34 @@
import { useReduceMotion } from "@renderer/hooks/biz/useReduceMotion"
import type { MotionProps } from "framer-motion"
import { m as M } from "framer-motion"
import { createElement, forwardRef } from "react"
const cacheMap = new Map<string, any>()
export const m: typeof M = new Proxy(M, {
get(target, p: string) {
const Component = target[p]
if (cacheMap.has(p)) {
return cacheMap.get(p)
}
const MotionComponent = forwardRef((props: MotionProps, ref) => {
const shouldReduceMotion = useReduceMotion()
const nextProps = { ...props }
if (shouldReduceMotion) {
if (props.exit) {
delete nextProps.exit
}
if (props.initial) {
nextProps.initial = true
}
}
return createElement(Component, { ...nextProps, ref })
})
cacheMap.set(p, MotionComponent)
return MotionComponent
},
})

View File

@ -1,12 +1,12 @@
import { useUISettingKey } from "@renderer/atoms"
import { useDark } from "@renderer/hooks"
import { cn } from "@renderer/lib/utils"
import { useUIStore } from "@renderer/store"
import { useMediaQuery } from "usehooks-ts"
export const Vibrancy: Component<
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
> = ({ className, children, ...rest }) => {
const opaqueSidebar = useUIStore((s) => s.opaqueSidebar)
const opaqueSidebar = useUISettingKey("opaqueSidebar")
const canVibrancy =
window.electron &&
window.electron.process.platform === "darwin" &&

View File

@ -1,6 +1,7 @@
import { m } from "@renderer/components/common/Motion"
import { cn } from "@renderer/lib/utils"
import type { Variants } from "framer-motion"
import { AnimatePresence, m } from "framer-motion"
import { AnimatePresence } from "framer-motion"
import { useCallback, useRef, useState } from "react"
import { MotionButtonBase } from "../button"

View File

@ -1,6 +1,6 @@
/* eslint-disable @eslint-react/dom/no-dangerously-set-innerhtml */
import { useUISettingSelector } from "@renderer/atoms"
import { cn } from "@renderer/lib/utils"
import { useUIStore } from "@renderer/store"
import type { FC } from "react"
import { useLayoutEffect, useMemo, useRef, useState } from "react"
import type {
@ -51,7 +51,7 @@ export const ShikiHighLighter: FC<ShikiProps> = (props) => {
const [loaded, setLoaded] = useState(false)
const codeTheme = useUIStore((s) => overrideTheme || s.codeHighlightTheme)
const codeTheme = useUISettingSelector((s) => overrideTheme || s.codeHighlightTheme)
useLayoutEffect(() => {
let isMounted = true
setLoaded(false)

View File

@ -1,5 +1,5 @@
import { m } from "@renderer/components/common/Motion"
import { stopPropagation } from "@renderer/lib/dom"
import { m } from "framer-motion"
import type { FC } from "react"
import { useState } from "react"
import { Mousewheel, Scrollbar, Virtual } from "swiper/modules"

View File

@ -1,5 +1,5 @@
import { getUISettings } from "@renderer/atoms"
import { jotaiStore } from "@renderer/lib/jotai"
import { useUIStore } from "@renderer/store"
import { useCallback, useContext, useEffect, useId, useRef } from "react"
import { useLocation } from "react-router-dom"
@ -31,9 +31,9 @@ export const useModalStack = (options?: ModalStackOptions) => {
} else {
// NOTE: The props of the Command Modal are immutable, so we'll just take the store value and inject it.
// There is no need to inject `overlay` props, this is rendered responsively based on ui changes.
const uiState = useUIStore.getState()
const uiSettings = getUISettings()
const modalConfig: Partial<ModalProps> = {
draggable: uiState.modalDraggable,
draggable: uiSettings.modalDraggable,
}
jotaiStore.set(modalStackAtom, (p) => {
const modalProps: ModalProps = {

View File

@ -1,8 +1,9 @@
import * as Dialog from "@radix-ui/react-dialog"
import { useUISettingKey } from "@renderer/atoms"
import { m } from "@renderer/components/common/Motion"
import { stopPropagation } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
import { useUIStore } from "@renderer/store"
import { m, useAnimationControls, useDragControls } from "framer-motion"
import { useAnimationControls, useDragControls } from "framer-motion"
import { useSetAtom } from "jotai"
import type { PointerEventHandler, SyntheticEvent } from "react"
import {
@ -60,7 +61,8 @@ export const ModalInternal: Component<{
// opaque: state.modalOpaque,
// })),
// )
const opaque = useUIStore((state) => state.modalOpaque)
// const opaque = useUIStore((state) => state.modalOpaque)
const opaque = useUISettingKey("modalOpaque")
const {
CustomModalComponent,

View File

@ -1,4 +1,4 @@
import { m } from "framer-motion"
import { m } from "@renderer/components/common/Motion"
import { RootPortal } from "../../portal"

View File

@ -1,10 +1,11 @@
import { useUIStore } from "@renderer/store"
import { useUISettingKey } from "@renderer/atoms"
import { AnimatePresence } from "framer-motion"
import { useAtomValue } from "jotai"
import type { FC, PropsWithChildren } from "react"
import { modalStackAtom } from "./atom"
import { MODAL_STACK_Z_INDEX } from "./constants"
import { useDismissAllWhenRouterChange } from "./hooks"
// import { useDismissAllWhenRouterChange } from "./hooks"
import { ModalInternal } from "./modal"
import { ModalOverlay } from "./overlay"
@ -20,9 +21,8 @@ const ModalStack = () => {
const stack = useAtomValue(modalStackAtom)
// Vite HMR issue
// useDismissAllWhenRouterChange()
const modalSettingOverlay = useUIStore((state) => state.modalOverlay)
useDismissAllWhenRouterChange()
const modalSettingOverlay = useUISettingKey("modalOverlay")
const forceOverlay = stack.some((item) => item.overlay)

View File

@ -1,8 +1,8 @@
import { createAtomHooks, jotaiStore } from "@renderer/lib/jotai"
import { buildStorageNS } from "@renderer/lib/ns"
import { getStorageNS } from "@renderer/lib/ns"
import { atomWithStorage } from "jotai/utils"
const SHOULD_USE_INDEXED_DB_KEY = buildStorageNS("shouldUseIndexedDB")
const SHOULD_USE_INDEXED_DB_KEY = getStorageNS("shouldUseIndexedDB")
export const [
__shouldUseIndexedDBAtom,

View File

@ -0,0 +1,8 @@
import { useUISettingKey } from "@renderer/atoms/ui"
import { useReducedMotion } from "framer-motion"
export const useReduceMotion = () => {
const appReduceMotion = useUISettingKey("reduceMotion")
const reduceMotion = useReducedMotion()
return appReduceMotion || reduceMotion
}

View File

@ -1,4 +1,4 @@
import { buildStorageNS } from "./ns"
import { getStorageNS } from "./ns"
export const levels = {
view: "view",
@ -79,7 +79,7 @@ export const APP_NAME = "Follow"
/// Feed
export const FEED_COLLECTION_LIST = "collections"
/// Local storage keys
export const QUERY_PERSIST_KEY = buildStorageNS("REACT_QUERY_OFFLINE_CACHE")
export const QUERY_PERSIST_KEY = getStorageNS("REACT_QUERY_OFFLINE_CACHE")
/// Route Keys
export const ROUTE_FEED_PENDING = "pending"

View File

@ -1,2 +1,6 @@
const ns = "follow"
export const buildStorageNS = (key: string) => `${ns}:${key}`
export const getStorageNS = (key: string) => `${ns}:${key}`
/**
* @deprecated Use `getStorageNS` instead.
*/
export const buildStorageKey = getStorageNS

View File

@ -1,5 +1,6 @@
import { useMainContainerElement } from "@renderer/atoms"
import { useUser } from "@renderer/atoms/user"
import { m } from "@renderer/components/common/Motion"
import { ActionButton, StyledButton } from "@renderer/components/ui/button"
import {
Popover,
@ -16,7 +17,7 @@ import {
} from "@renderer/hooks/biz/useRouteParams"
import { apiClient } from "@renderer/lib/api-fetch"
import { ROUTE_FEED_PENDING, views } from "@renderer/lib/constants"
import { buildStorageNS } from "@renderer/lib/ns"
import { getStorageNS } from "@renderer/lib/ns"
import { shortcuts } from "@renderer/lib/shortcuts"
import { cn, getEntriesParams, getOS, isBizId } from "@renderer/lib/utils"
import { useEntries } from "@renderer/queries/entries"
@ -32,7 +33,6 @@ import {
useEntryIdsByFeedIdOrView,
} from "@renderer/store/entry/hooks"
import type { HTMLMotionProps } from "framer-motion"
import { m } from "framer-motion"
import { useAtom, useAtomValue } from "jotai"
import { atomWithStorage } from "jotai/utils"
import { debounce } from "lodash-es"
@ -56,7 +56,7 @@ import { LoadingCircle } from "../../components/ui/loading"
import { EntryItem } from "./item"
const unreadOnlyAtom = atomWithStorage<boolean>(
buildStorageNS("entry-unreadonly"),
getStorageNS("entry-unreadonly"),
true,
undefined,
{

View File

@ -1,3 +1,5 @@
import { useUISettingKey } from "@renderer/atoms"
import { m } from "@renderer/components/common/Motion"
import { Logo } from "@renderer/components/icons/logo"
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
import { useBizQuery } from "@renderer/hooks"
@ -9,8 +11,7 @@ import {
WrappedElementProvider,
} from "@renderer/providers/wrapped-element-provider"
import { Queries } from "@renderer/queries"
import { useEntry, useFeedHeaderTitle, useUIStore } from "@renderer/store"
import { m } from "framer-motion"
import { useEntry, useFeedHeaderTitle } from "@renderer/store"
import { useEffect, useState } from "react"
import { LoadingCircle } from "../../components/ui/loading"
@ -43,9 +44,7 @@ function EntryContentRender({ entryId }: { entryId: string }) {
const entry = useEntry(entryId)
const [content, setContent] = useState<JSX.Element>()
const readerRenderInlineStyle = useUIStore(
(state) => state.readerRenderInlineStyle,
)
const readerRenderInlineStyle = useUISettingKey("readerRenderInlineStyle")
useEffect(() => {
// Fallback data, if local data is broken should fallback to cached query data.
const processContent = entry?.entries.content ?? data?.entries.content
@ -85,7 +84,7 @@ function EntryContentRender({ entryId }: { entryId: string }) {
},
)
const readerFontFamily = useUIStore((state) => state.readerFontFamily)
const readerFontFamily = useUISettingKey("readerFontFamily")
if (!entry) return null

View File

@ -4,6 +4,7 @@ import { Logo } from "@renderer/components/icons/logo"
import { ActionButton } from "@renderer/components/ui/button"
import { ProfileButton } from "@renderer/components/user-button"
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
import { useReduceMotion } from "@renderer/hooks/biz/useReduceMotion"
import { APP_NAME, levels, views } from "@renderer/lib/constants"
import { stopPropagation } from "@renderer/lib/dom"
import { Routes } from "@renderer/lib/enum"
@ -99,6 +100,7 @@ export function FeedColumn() {
const normalStyle =
!window.electron || window.electron.process.platform !== "darwin"
const reduceMotion = useReduceMotion()
return (
<Vibrancy
className="flex h-full flex-col gap-3 pt-2.5"
@ -158,7 +160,7 @@ export function FeedColumn() {
))}
</div>
<div className="size-full overflow-hidden" ref={carouselRef}>
<m.div className="flex h-full" style={{ x: spring }}>
<m.div className="flex h-full" style={{ x: reduceMotion ? -active * 256 : spring }}>
{views.map((item, index) => (
<section
key={item.name}

View File

@ -1,3 +1,4 @@
import { useUISettingKey } from "@renderer/atoms"
import { useBizQuery } from "@renderer/hooks"
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
import { useRouteFeedId } from "@renderer/hooks/biz/useRouteParams"
@ -11,7 +12,6 @@ import type { SubscriptionPlainModel } from "@renderer/store"
import {
getFeedById,
useSubscriptionByView,
useUIStore,
useUnreadStore,
} from "@renderer/store"
import { useMemo, useState } from "react"
@ -123,7 +123,7 @@ export function FeedList({
const feedId = useRouteFeedId()
const navigate = useNavigateEntry()
const showUnreadCount = useUIStore((state) => state.sidebarShowUnreadCount)
const showUnreadCount = useUISettingKey("sidebarShowUnreadCount")
return (
<div className={cn(className, "font-medium")}>

View File

@ -43,7 +43,7 @@ const Close = () => {
const { dismiss } = useCurrentModal()
return (
<MotionButtonBase className="absolute right-8 top-7" onClick={dismiss}>
<MotionButtonBase className="absolute right-8 top-7 z-[99]" onClick={dismiss}>
<i className="i-mgc-close-cute-re" />
</MotionButtonBase>
)

View File

@ -1,12 +1,12 @@
import { useUISettingSelector } from "@renderer/atoms"
import { m } from "@renderer/components/common/Motion"
import { Logo } from "@renderer/components/icons/logo"
import { APP_NAME } from "@renderer/lib/constants"
import { preventDefault } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
import { useUIStore } from "@renderer/store"
import { m, useDragControls } from "framer-motion"
import { useDragControls } from "framer-motion"
import type { PointerEventHandler, PropsWithChildren } from "react"
import { useCallback, useEffect } from "react"
import { useShallow } from "zustand/react/shallow"
import { settings } from "../constants"
import { SettingsSidebarTitle } from "../title"
@ -31,8 +31,8 @@ export function SettingModalLayout(
}
}, [])
const { draggable, overlay } = useUIStore(
useShallow((state) => ({
const { draggable, overlay } = useUISettingSelector(
((state) => ({
draggable: state.modalDraggable,
overlay: state.modalOverlay,
})),

View File

@ -1,3 +1,9 @@
import {
setUISetting,
useUISettingKey,
useUISettingSelector,
useUISettingValue,
} from "@renderer/atoms"
import {
Select,
SelectContent,
@ -8,12 +14,11 @@ import {
import { useDark } from "@renderer/hooks"
import { tipcClient } from "@renderer/lib/client"
import { getOS } from "@renderer/lib/utils"
import { uiActions, useUIStore } from "@renderer/store"
import { useQuery } from "@tanstack/react-query"
import { useCallback } from "react"
import { bundledThemes } from "shiki/themes"
import { SettingSwitch } from "../control"
import { SettingDescription, SettingSwitch } from "../control"
import { SettingSectionTitle } from "../section"
import { SettingsTitle } from "../title"
@ -23,7 +28,7 @@ export const SettingAppearance = () => {
toggleDark()
}, [])
const state = useUIStore()
const state = useUISettingValue()
const onlyMacos = window.electron && getOS() === "macOS"
return (
@ -40,7 +45,7 @@ export const SettingAppearance = () => {
label="Opaque Sidebars"
checked={state.opaqueSidebar}
onCheckedChange={(checked) => {
uiActions.set("opaqueSidebar", checked)
setUISetting("opaqueSidebar", checked)
}}
/>
)}
@ -54,7 +59,7 @@ export const SettingAppearance = () => {
label="Dock Badge"
checked={state.showDockBadge}
onCheckedChange={(c) => {
uiActions.set("showDockBadge", c)
setUISetting("showDockBadge", c)
}}
/>
)}
@ -62,7 +67,7 @@ export const SettingAppearance = () => {
label="Show sidebar unread count"
checked={state.sidebarShowUnreadCount}
onCheckedChange={(c) => {
uiActions.set("sidebarShowUnreadCount", c)
setUISetting("sidebarShowUnreadCount", c)
}}
/>
@ -70,14 +75,14 @@ export const SettingAppearance = () => {
label="Modal draggable"
checked={state.modalDraggable}
onCheckedChange={(c) => {
uiActions.set("modalDraggable", c)
setUISetting("modalDraggable", c)
}}
/> */}
{/* <SettingSwitch
label="Modal opaque"
checked={state.modalOpaque}
onCheckedChange={(c) => {
uiActions.set("modalOpaque", c)
setUISetting("modalOpaque", c)
}}
/> */}
@ -88,22 +93,35 @@ export const SettingAppearance = () => {
label="Render inline style"
checked={state.readerRenderInlineStyle}
onCheckedChange={(c) => {
uiActions.set("readerRenderInlineStyle", c)
setUISetting("readerRenderInlineStyle", c)
}}
/>
<SettingSectionTitle title="Misc" />
<SettingSwitch
label="Show modal overlay"
checked={state.modalOverlay}
onCheckedChange={(c) => {
uiActions.set("modalOverlay", c)
setUISetting("modalOverlay", c)
}}
/>
<SettingSwitch
label="Reduce motion"
checked={state.reduceMotion}
onCheckedChange={(c) => {
setUISetting("reduceMotion", c)
}}
/>
<SettingDescription>
Enabling this option will reduce the motion of the element to improve
performance and device life, and if it is disabled, it will adapt to the
system settings.
</SettingDescription>
</div>
)
}
const ShikiTheme = () => {
const codeHighlightTheme = useUIStore((state) => state.codeHighlightTheme)
const codeHighlightTheme = useUISettingKey("codeHighlightTheme")
return (
<div className="mb-3 flex items-center justify-between">
<span className="shrink-0 text-sm font-medium">Code Highlight Theme</span>
@ -111,19 +129,18 @@ const ShikiTheme = () => {
defaultValue="github-dark"
value={codeHighlightTheme}
onValueChange={(value) => {
uiActions.set("codeHighlightTheme", value)
setUISetting("codeHighlightTheme", value)
}}
>
<SelectTrigger size="sm" className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent className="h-64">
{Object.keys(bundledThemes)
?.map((theme) => (
<SelectItem key={theme} value={theme}>
{theme}
</SelectItem>
))}
{Object.keys(bundledThemes)?.map((theme) => (
<SelectItem key={theme} value={theme}>
{theme}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
@ -135,7 +152,7 @@ const Fonts = () => {
queryFn: () => tipcClient?.getSystemFonts(),
queryKey: ["systemFonts"],
})
const readerFontFamily = useUIStore(
const readerFontFamily = useUISettingSelector(
(state) => state.readerFontFamily || "SN Pro",
)
return (
@ -145,7 +162,7 @@ const Fonts = () => {
defaultValue="SN Pro"
value={readerFontFamily}
onValueChange={(value) => {
uiActions.set("readerFontFamily", value)
setUISetting("readerFontFamily", value)
}}
>
<SelectTrigger size="sm" className="w-48">
@ -173,7 +190,7 @@ const textSizeMap = {
}
const TextSize = () => {
const uiTextSize = useUIStore((state) => state.uiTextSize)
const uiTextSize = useUISettingSelector((state) => state.uiTextSize)
return (
<div className="mt-1 flex items-center justify-between">
@ -182,7 +199,7 @@ const TextSize = () => {
defaultValue={textSizeMap.default.toString()}
value={uiTextSize.toString() || textSizeMap.default.toString()}
onValueChange={(value) => {
uiActions.set(
setUISetting(
"uiTextSize",
Number.parseInt(value) || textSizeMap.default,
)

View File

@ -48,7 +48,7 @@ export const SettingsTitle = ({
<div
className={cn(
"flex items-center gap-2 pb-2 pt-6 text-xl font-bold",
"sticky top-0 mb-4 bg-background",
"sticky top-0 z-[11] mb-4 bg-background",
className,
)}
>

View File

@ -1,8 +1,8 @@
import { getUISettings, setUISetting } from "@renderer/atoms"
import { useRouteView } from "@renderer/hooks/biz/useRouteParams"
import { views } from "@renderer/lib/constants"
import { cn } from "@renderer/lib/utils"
import { EntryColumn } from "@renderer/modules/entry-column"
import { uiActions, useUIStore } from "@renderer/store"
import { useMemo, useRef } from "react"
import { HotkeysProvider } from "react-hotkeys-hook"
import { useResizable } from "react-resizable-layout"
@ -12,8 +12,8 @@ export function Component() {
const containerRef = useRef<HTMLDivElement>(null)
// Memo this initial value to avoid re-render
// eslint-disable-next-line react-compiler/react-compiler
const entryColWidth = useMemo(() => useUIStore.getState().entryColWidth, [])
const entryColWidth = useMemo(() => getUISettings().entryColWidth, [])
const view = useRouteView()
const inWideMode = view ? views[view].wideMode : false
const { position, separatorProps } = useResizable({
@ -23,7 +23,7 @@ export function Component() {
initial: entryColWidth,
containerRef,
onResizeEnd({ position }) {
uiActions.set("entryColWidth", position)
setUISetting("entryColWidth", position)
},
})

View File

@ -1,9 +1,11 @@
import { initializeDefaultUISettings, useUISettingValue } from "@renderer/atoms"
import { tipcClient } from "@renderer/lib/client"
import { unreadActions, useUIStore } from "@renderer/store"
import { unreadActions } from "@renderer/store"
import { useEffect, useInsertionEffect } from "react"
initializeDefaultUISettings()
export const UISettingInitialize = () => {
const state = useUIStore()
const state = useUISettingValue()
useInsertionEffect(() => {
const root = document.documentElement

View File

@ -1,5 +1,4 @@
export * from "./entry"
export * from "./feed"
export * from "./subscription"
export * from "./ui"
export * from "./unread"

View File

@ -1,65 +0,0 @@
import { buildStorageNS } from "@renderer/lib/ns"
import {
createZustandStore,
getStoreActions,
localStorage,
} from "./utils/helper"
interface UIState {
entryColWidth: number
opaqueSidebar: boolean
// UI
uiTextSize: number
// Display counts
/** macOS only */
showDockBadge: boolean
sidebarShowUnreadCount: boolean
// modal
modalOverlay: boolean
modalDraggable: boolean
modalOpaque: boolean
// content
readerFontFamily: string
readerRenderInlineStyle: boolean
codeHighlightTheme: string
}
const createDefaultUIState = (): UIState => ({
entryColWidth: 340,
opaqueSidebar: false,
readerFontFamily: "SN Pro",
uiTextSize: 16,
showDockBadge: true,
sidebarShowUnreadCount: true,
modalOverlay: true,
modalDraggable: true,
modalOpaque: true,
readerRenderInlineStyle: false,
codeHighlightTheme: "github-dark",
})
interface UIActions {
clear: () => void
set: <T extends keyof UIState>(key: T, value: UIState[T]) => void
}
const storageKey = buildStorageNS("ui")
export const useUIStore = createZustandStore<UIState & UIActions>(storageKey, {
version: 1,
storage: localStorage,
})((set) => ({
...createDefaultUIState(),
clear() {
set(createDefaultUIState())
},
set(key, value) {
set({ [key]: value })
},
}))
export const uiActions = getStoreActions(useUIStore)

View File

@ -1,9 +1,9 @@
import { clearUISettings } from "@renderer/atoms"
import { browserDB } from "@renderer/database"
import { entryActions } from "../entry"
import { feedActions } from "../feed"
import { subscriptionActions } from "../subscription"
import { uiActions } from "../ui"
import { unreadActions } from "../unread"
export const clearLocalPersistStoreData = () => {
@ -12,11 +12,12 @@ export const clearLocalPersistStoreData = () => {
entryActions,
subscriptionActions,
unreadActions,
uiActions,
feedActions,
].forEach((actions) => {
actions.clear()
})
clearUISettings()
browserDB.delete()
}