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:
parent
16f9645758
commit
48e73d7e83
|
|
@ -1,2 +1,5 @@
|
|||
export * from "./dom"
|
||||
export * from "./route"
|
||||
export * from "./sidebar"
|
||||
export * from "./ui"
|
||||
export * from "./user"
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
})
|
||||
|
|
@ -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" &&
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { m } from "framer-motion"
|
||||
import { m } from "@renderer/components/common/Motion"
|
||||
|
||||
import { RootPortal } from "../../portal"
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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")}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
export * from "./entry"
|
||||
export * from "./feed"
|
||||
export * from "./subscription"
|
||||
export * from "./ui"
|
||||
export * from "./unread"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue