Skip to content

Commit

Permalink
feat: reduce motion (#101)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Innei authored Jul 1, 2024
1 parent 16f9645 commit 48e73d7
Show file tree
Hide file tree
Showing 28 changed files with 219 additions and 137 deletions.
3 changes: 3 additions & 0 deletions src/renderer/src/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from "./dom"
export * from "./route"
export * from "./sidebar"
export * from "./ui"
export * from "./user"
75 changes: 75 additions & 0 deletions src/renderer/src/atoms/ui.ts
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 58 in src/renderer/src/atoms/ui.ts

View workflow job for this annotation

GitHub Actions / Lint and Typecheck (18.x)

Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer
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())
}
34 changes: 34 additions & 0 deletions src/renderer/src/components/common/Motion.tsx
Original file line number Diff line number Diff line change
@@ -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
},
})
4 changes: 2 additions & 2 deletions src/renderer/src/components/ui/background/vibrancy.tsx
Original file line number Diff line number Diff line change
@@ -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" &&
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/components/ui/image/preview-image.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/src/components/ui/modal/stacked/hooks.tsx
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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 = {
Expand Down
8 changes: 5 additions & 3 deletions src/renderer/src/components/ui/modal/stacked/modal.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/components/ui/modal/stacked/overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { m } from "framer-motion"
import { m } from "@renderer/components/common/Motion"

import { RootPortal } from "../../portal"

Expand Down
8 changes: 4 additions & 4 deletions src/renderer/src/components/ui/modal/stacked/provider.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/database/hooks.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/src/hooks/biz/useReduceMotion.ts
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions src/renderer/src/lib/constants.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildStorageNS } from "./ns"
import { getStorageNS } from "./ns"

export const levels = {
view: "view",
Expand Down Expand Up @@ -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"
6 changes: 5 additions & 1 deletion src/renderer/src/lib/ns.ts
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions src/renderer/src/modules/entry-column/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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,
{
Expand Down
11 changes: 5 additions & 6 deletions src/renderer/src/modules/entry-content/index.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -85,7 +84,7 @@ function EntryContentRender({ entryId }: { entryId: string }) {
},
)

const readerFontFamily = useUIStore((state) => state.readerFontFamily)
const readerFontFamily = useUISettingKey("readerFontFamily")

if (!entry) return null

Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/modules/feed-column/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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}
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/modules/feed-column/list.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -11,7 +12,6 @@ import type { SubscriptionPlainModel } from "@renderer/store"
import {
getFeedById,
useSubscriptionByView,
useUIStore,
useUnreadStore,
} from "@renderer/store"
import { useMemo, useState } from "react"
Expand Down Expand Up @@ -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")}>
Expand Down
Loading

0 comments on commit 48e73d7

Please sign in to comment.