feat(desktop): simplify settings via enhance settings toggle
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
92f76b93d5
commit
217e1a8f2a
|
|
@ -419,5 +419,10 @@
|
|||
"wallet.withdraw.modalTitle": "Withdraw Power",
|
||||
"wallet.withdraw.submitButton": "Submit",
|
||||
"wallet.withdraw.success": "Withdrawal successful!",
|
||||
"wallet.withdraw.toRss3Label": "Withdraw as RSS3"
|
||||
"wallet.withdraw.toRss3Label": "Withdraw as RSS3",
|
||||
"general.advanced": "Advanced",
|
||||
"general.enhanced.label": "Enhanced Settings",
|
||||
"general.enhanced.description": "Enabling the enhanced settings offers more customization options, but it may also introduce unforeseen issues.",
|
||||
"general.enhanced.enabled.tip": "Enhanced settings are enabled, you can disable them in the General settings - Advanced.",
|
||||
"general.enhanced.disabled.tip": "Enhanced settings are disabled, you can enable them in the General settings - Advanced."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,65 +1,181 @@
|
|||
import { createSettingAtom } from "@follow/atoms/helper/setting.js"
|
||||
import type { SupportedLanguages } from "@follow/models"
|
||||
import type { GeneralSettings } from "@follow/shared/interface/settings"
|
||||
import { defaultGeneralSettings } from "@follow/shared/settings/defaults"
|
||||
import { enhancedGeneralSettingKeys } from "@follow/shared/settings/enhanced"
|
||||
import type { GeneralSettings } from "@follow/shared/settings/interface"
|
||||
import { useCallback, useMemo } from "react"
|
||||
|
||||
import { jotaiStore } from "~/lib/jotai"
|
||||
|
||||
export const DEFAULT_ACTION_LANGUAGE = "default"
|
||||
|
||||
const createDefaultSettings = (): GeneralSettings => ({
|
||||
// App
|
||||
appLaunchOnStartup: false,
|
||||
language: "en",
|
||||
actionLanguage: DEFAULT_ACTION_LANGUAGE,
|
||||
const createDefaultSettings = (): GeneralSettings => defaultGeneralSettings
|
||||
|
||||
// mobile app
|
||||
startupScreen: "timeline",
|
||||
// Data control
|
||||
dataPersist: true,
|
||||
sendAnonymousData: true,
|
||||
showQuickTimeline: true,
|
||||
|
||||
autoGroup: true,
|
||||
|
||||
// view
|
||||
unreadOnly: true,
|
||||
// mark unread
|
||||
scrollMarkUnread: true,
|
||||
hoverMarkUnread: true,
|
||||
renderMarkUnread: false,
|
||||
// UX
|
||||
groupByDate: true,
|
||||
autoExpandLongSocialMedia: false,
|
||||
|
||||
// Secure
|
||||
jumpOutLinkWarn: true,
|
||||
// TTS
|
||||
voice: "en-US-AndrewMultilingualNeural",
|
||||
})
|
||||
|
||||
export const {
|
||||
useSettingKey: useGeneralSettingKey,
|
||||
useSettingSelector: useGeneralSettingSelector,
|
||||
useSettingKeys: useGeneralSettingKeys,
|
||||
const {
|
||||
useSettingKey: useGeneralSettingKeyInternal,
|
||||
useSettingSelector: useGeneralSettingSelectorInternal,
|
||||
useSettingKeys: useGeneralSettingKeysInternal,
|
||||
setSetting: setGeneralSetting,
|
||||
clearSettings: clearGeneralSettings,
|
||||
initializeDefaultSettings: initializeDefaultGeneralSettings,
|
||||
getSettings: getGeneralSettings,
|
||||
useSettingValue: useGeneralSettingValue,
|
||||
getSettings: getGeneralSettingsInternal,
|
||||
useSettingValue: useGeneralSettingValueInternal,
|
||||
|
||||
settingAtom: __generalSettingAtom,
|
||||
} = createSettingAtom("general", createDefaultSettings)
|
||||
|
||||
const [
|
||||
useGeneralSettingKey,
|
||||
useGeneralSettingSelector,
|
||||
useGeneralSettingKeys,
|
||||
getGeneralSettings,
|
||||
useGeneralSettingValue,
|
||||
] = hookEnhancedSettings(
|
||||
useGeneralSettingKeyInternal,
|
||||
useGeneralSettingSelectorInternal,
|
||||
useGeneralSettingKeysInternal,
|
||||
getGeneralSettingsInternal,
|
||||
useGeneralSettingValueInternal,
|
||||
|
||||
enhancedGeneralSettingKeys,
|
||||
defaultGeneralSettings,
|
||||
)
|
||||
export {
|
||||
__generalSettingAtom,
|
||||
clearGeneralSettings,
|
||||
getGeneralSettings,
|
||||
initializeDefaultGeneralSettings,
|
||||
setGeneralSetting,
|
||||
useGeneralSettingKey,
|
||||
useGeneralSettingKeys,
|
||||
useGeneralSettingSelector,
|
||||
useGeneralSettingValue,
|
||||
}
|
||||
export function hookEnhancedSettings<
|
||||
T1 extends (key: any) => any,
|
||||
T2 extends (selector: (s: any) => any) => any,
|
||||
T3 extends (keys: any) => any,
|
||||
T4 extends () => any,
|
||||
T5 extends () => any,
|
||||
>(
|
||||
useSettingKey: T1,
|
||||
useSettingSelector: T2,
|
||||
useSettingKeys: T3,
|
||||
getSettings: T4,
|
||||
useSettingValue: T5,
|
||||
|
||||
enhancedSettingKeys: Set<string>,
|
||||
defaultSettings: Record<string, any>,
|
||||
): [T1, T2, T3, T4, T5] {
|
||||
const useNextSettingKey = (key: string) => {
|
||||
const enableEnhancedSettings = useGeneralSettingKeyInternal("enhancedSettings")
|
||||
const settingValue = useSettingKey(key)
|
||||
const shouldBackToDefault = enhancedSettingKeys.has(key) && !enableEnhancedSettings
|
||||
if (!shouldBackToDefault) {
|
||||
return settingValue
|
||||
}
|
||||
|
||||
return defaultSettings[key] === undefined ? settingValue : defaultSettings[key]
|
||||
}
|
||||
|
||||
const useNextSettingSelector = (selector: (s: any) => any) => {
|
||||
const enableEnhancedSettings = useGeneralSettingKeyInternal("enhancedSettings")
|
||||
return useSettingSelector(
|
||||
useCallback(
|
||||
(settings) => {
|
||||
if (enableEnhancedSettings) {
|
||||
return selector(settings)
|
||||
}
|
||||
|
||||
const enhancedSettings = { ...settings }
|
||||
for (const key of enhancedSettingKeys) {
|
||||
if (defaultSettings[key] !== undefined) {
|
||||
enhancedSettings[key] = defaultSettings[key]
|
||||
}
|
||||
}
|
||||
|
||||
return selector(enhancedSettings)
|
||||
},
|
||||
[enableEnhancedSettings, selector],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const useNextSettingKeys = (keys: string[]) => {
|
||||
const enableEnhancedSettings = useGeneralSettingKeyInternal("enhancedSettings")
|
||||
const rawSettingValues = useSettingKeys(keys)
|
||||
return useMemo(() => {
|
||||
if (enableEnhancedSettings) {
|
||||
return rawSettingValues
|
||||
}
|
||||
|
||||
const result = { ...rawSettingValues }
|
||||
for (const key of keys) {
|
||||
if (enhancedSettingKeys.has(key) && defaultSettings[key] !== undefined) {
|
||||
result[key] = defaultSettings[key]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}, [enableEnhancedSettings, keys, rawSettingValues])
|
||||
}
|
||||
|
||||
const getNextSettings = () => {
|
||||
const settings = getSettings()
|
||||
const enableEnhancedSettings = jotaiStore.get(__generalSettingAtom).enhancedSettings
|
||||
|
||||
if (enableEnhancedSettings) {
|
||||
return settings
|
||||
}
|
||||
|
||||
const enhancedSettings = { ...settings }
|
||||
for (const key of enhancedSettingKeys) {
|
||||
if (defaultSettings[key] !== undefined) {
|
||||
enhancedSettings[key] = defaultSettings[key]
|
||||
}
|
||||
}
|
||||
|
||||
return enhancedSettings
|
||||
}
|
||||
|
||||
const useNextSettingValue = () => {
|
||||
const settingValues = useSettingValue()
|
||||
const enableEnhancedSettings = useGeneralSettingKeyInternal("enhancedSettings")
|
||||
|
||||
return useMemo(() => {
|
||||
if (enableEnhancedSettings) {
|
||||
return settingValues
|
||||
}
|
||||
|
||||
const result = { ...settingValues }
|
||||
for (const key of enhancedSettingKeys) {
|
||||
if (defaultSettings[key] !== undefined) {
|
||||
result[key] = defaultSettings[key]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}, [enableEnhancedSettings, settingValues])
|
||||
}
|
||||
return [
|
||||
useNextSettingKey as T1,
|
||||
useNextSettingSelector as T2,
|
||||
useNextSettingKeys as T3,
|
||||
getNextSettings as T4,
|
||||
useNextSettingValue as T5,
|
||||
]
|
||||
}
|
||||
|
||||
export function useActionLanguage() {
|
||||
const actionLanguage = useGeneralSettingSelector((s) => s.actionLanguage)
|
||||
const language = useGeneralSettingSelector((s) => s.language)
|
||||
const actionLanguage = useGeneralSettingSelectorInternal((s) => s.actionLanguage)
|
||||
const language = useGeneralSettingSelectorInternal((s) => s.language)
|
||||
return (
|
||||
actionLanguage === DEFAULT_ACTION_LANGUAGE ? language : actionLanguage
|
||||
) as SupportedLanguages
|
||||
}
|
||||
|
||||
export const subscribeShouldUseIndexedDB = (callback: (value: boolean) => void) =>
|
||||
jotaiStore.sub(__generalSettingAtom, () => callback(getGeneralSettings().dataPersist))
|
||||
jotaiStore.sub(__generalSettingAtom, () => callback(getGeneralSettingsInternal().dataPersist))
|
||||
|
||||
export const generalServerSyncWhiteListKeys: (keyof GeneralSettings)[] = [
|
||||
"appLaunchOnStartup",
|
||||
|
|
|
|||
|
|
@ -1,34 +1,8 @@
|
|||
import { createSettingAtom } from "@follow/atoms/helper/setting.js"
|
||||
import type { IntegrationSettings } from "@follow/shared/interface/settings"
|
||||
import { defaultIntegrationSettings } from "@follow/shared/settings/defaults"
|
||||
import type { IntegrationSettings } from "@follow/shared/settings/interface"
|
||||
|
||||
export const createDefaultSettings = (): IntegrationSettings => ({
|
||||
// eagle
|
||||
enableEagle: true,
|
||||
|
||||
// readwise
|
||||
enableReadwise: false,
|
||||
readwiseToken: "",
|
||||
|
||||
// instapaper
|
||||
enableInstapaper: false,
|
||||
instapaperUsername: "",
|
||||
instapaperPassword: "",
|
||||
|
||||
// obsidian
|
||||
enableObsidian: false,
|
||||
obsidianVaultPath: "",
|
||||
|
||||
// outline
|
||||
enableOutline: false,
|
||||
outlineEndpoint: "",
|
||||
outlineToken: "",
|
||||
outlineCollection: "",
|
||||
|
||||
// readeck
|
||||
enableReadeck: false,
|
||||
readeckEndpoint: "",
|
||||
readeckToken: "",
|
||||
})
|
||||
export const createDefaultSettings = (): IntegrationSettings => defaultIntegrationSettings
|
||||
|
||||
export const {
|
||||
useSettingKey: useIntegrationSettingKey,
|
||||
|
|
|
|||
|
|
@ -1,49 +1,17 @@
|
|||
import { createSettingAtom } from "@follow/atoms/helper/setting.js"
|
||||
import type { UISettings } from "@follow/shared/interface/settings"
|
||||
import { defaultUISettings } from "@follow/shared/settings/defaults"
|
||||
import { enhancedUISettingKeys } from "@follow/shared/settings/enhanced"
|
||||
import type { UISettings } from "@follow/shared/settings/interface"
|
||||
import { jotaiStore } from "@follow/utils/jotai"
|
||||
import { atom, useAtomValue, useSetAtom } from "jotai"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
|
||||
import { DEFAULT_ACTION_ORDER } from "~/modules/customize-toolbar/constant"
|
||||
|
||||
import { hookEnhancedSettings } from "./general"
|
||||
|
||||
export const createDefaultSettings = (): UISettings => ({
|
||||
// Sidebar
|
||||
entryColWidth: 356,
|
||||
feedColWidth: 256,
|
||||
hideExtraBadge: false,
|
||||
|
||||
opaqueSidebar: false,
|
||||
sidebarShowUnreadCount: true,
|
||||
thumbnailRatio: "square",
|
||||
|
||||
// Global UI
|
||||
uiTextSize: 16,
|
||||
// System
|
||||
showDockBadge: true,
|
||||
// Misc
|
||||
modalOverlay: true,
|
||||
modalDraggable: true,
|
||||
modalOpaque: true,
|
||||
reduceMotion: false,
|
||||
usePointerCursor: false,
|
||||
|
||||
// Font
|
||||
uiFontFamily: "SN Pro",
|
||||
readerFontFamily: "inherit",
|
||||
contentFontSize: 16,
|
||||
dateFormat: "default",
|
||||
contentLineHeight: 1.75,
|
||||
// Content
|
||||
readerRenderInlineStyle: false,
|
||||
codeHighlightThemeLight: "github-light",
|
||||
codeHighlightThemeDark: "github-dark",
|
||||
guessCodeLanguage: true,
|
||||
hideRecentReader: false,
|
||||
customCSS: "",
|
||||
|
||||
// View
|
||||
pictureViewMasonry: true,
|
||||
wideMode: false,
|
||||
...defaultUISettings,
|
||||
|
||||
// Action Order
|
||||
toolbarOrder: DEFAULT_ACTION_ORDER,
|
||||
|
|
@ -51,18 +19,41 @@ export const createDefaultSettings = (): UISettings => ({
|
|||
|
||||
const zenModeAtom = atom(false)
|
||||
|
||||
export const {
|
||||
useSettingKey: useUISettingKey,
|
||||
useSettingSelector: useUISettingSelector,
|
||||
useSettingKeys: useUISettingKeys,
|
||||
const {
|
||||
useSettingKey: useUISettingKeyInternal,
|
||||
useSettingSelector: useUISettingSelectorInternal,
|
||||
useSettingKeys: useUISettingKeysInternal,
|
||||
setSetting: setUISetting,
|
||||
clearSettings: clearUISettings,
|
||||
initializeDefaultSettings: initializeDefaultUISettings,
|
||||
getSettings: getUISettings,
|
||||
useSettingValue: useUISettingValue,
|
||||
getSettings: getUISettingsInternal,
|
||||
useSettingValue: useUISettingValueInternal,
|
||||
settingAtom: __uiSettingAtom,
|
||||
} = createSettingAtom("ui", createDefaultSettings)
|
||||
|
||||
const [useUISettingKey, useUISettingSelector, useUISettingKeys, getUISettings, useUISettingValue] =
|
||||
hookEnhancedSettings(
|
||||
useUISettingKeyInternal,
|
||||
useUISettingSelectorInternal,
|
||||
useUISettingKeysInternal,
|
||||
getUISettingsInternal,
|
||||
useUISettingValueInternal,
|
||||
|
||||
enhancedUISettingKeys,
|
||||
defaultUISettings,
|
||||
)
|
||||
export {
|
||||
__uiSettingAtom,
|
||||
clearUISettings,
|
||||
getUISettings,
|
||||
initializeDefaultUISettings,
|
||||
setUISetting,
|
||||
useUISettingKey,
|
||||
useUISettingKeys,
|
||||
useUISettingSelector,
|
||||
useUISettingValue,
|
||||
}
|
||||
|
||||
export const uiServerSyncWhiteListKeys: (keyof UISettings)[] = [
|
||||
"uiFontFamily",
|
||||
"readerFontFamily",
|
||||
|
|
|
|||
|
|
@ -30,3 +30,72 @@ export const softBouncePreset: Spring = {
|
|||
damping: 10,
|
||||
stiffness: 100,
|
||||
}
|
||||
|
||||
/**
|
||||
* A smooth spring with a predefined duration and no bounce.
|
||||
*/
|
||||
export const smoothPreset: Spring = {
|
||||
type: "spring",
|
||||
duration: 0.5,
|
||||
bounce: 0,
|
||||
}
|
||||
|
||||
/**
|
||||
* A smooth spring with a predefined duration and no bounce that can be tuned.
|
||||
*
|
||||
* @param duration The perceptual duration, which defines the pace of the spring.
|
||||
* @param extraBounce How much additional bounce should be added to the base bounce of 0.
|
||||
*/
|
||||
export function smooth(duration = 0.5, extraBounce = 0): Spring {
|
||||
return {
|
||||
type: "spring",
|
||||
duration,
|
||||
bounce: extraBounce,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A spring with a predefined duration and small amount of bounce that feels more snappy.
|
||||
*/
|
||||
export const snappyPreset: Spring = {
|
||||
type: "spring",
|
||||
duration: 0.5,
|
||||
bounce: 0.15,
|
||||
}
|
||||
|
||||
/**
|
||||
* A spring with a predefined duration and small amount of bounce that feels more snappy and can be tuned.
|
||||
*
|
||||
* @param duration The perceptual duration, which defines the pace of the spring.
|
||||
* @param extraBounce How much additional bounciness should be added to the base bounce of 0.15.
|
||||
*/
|
||||
export function snappy(duration = 0.5, extraBounce = 0): Spring {
|
||||
return {
|
||||
type: "spring",
|
||||
duration,
|
||||
bounce: 0.15 + extraBounce,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A spring with a predefined duration and higher amount of bounce.
|
||||
*/
|
||||
export const bouncyPreset: Spring = {
|
||||
type: "spring",
|
||||
duration: 0.5,
|
||||
bounce: 0.3,
|
||||
}
|
||||
|
||||
/**
|
||||
* A spring with a predefined duration and higher amount of bounce that can be tuned.
|
||||
*
|
||||
* @param duration The perceptual duration, which defines the pace of the spring.
|
||||
* @param extraBounce How much additional bounce should be added to the base bounce of 0.3.
|
||||
*/
|
||||
export function bouncy(duration = 0.5, extraBounce = 0): Spring {
|
||||
return {
|
||||
type: "spring",
|
||||
duration,
|
||||
bounce: 0.3 + extraBounce,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { cn } from "@follow/utils/utils"
|
||||
import type { Target } from "framer-motion"
|
||||
import type { Spring, Target } from "framer-motion"
|
||||
import { AnimatePresence, m } from "framer-motion"
|
||||
import * as React from "react"
|
||||
import { cloneElement, useEffect, useState } from "react"
|
||||
|
||||
import { smoothPreset } from "~/components/ui/constants/spring"
|
||||
|
||||
type TransitionType = {
|
||||
initial: Target | boolean
|
||||
animate: Target
|
||||
|
|
@ -80,3 +82,38 @@ export const IconOpacityTransition = createIconTransition({
|
|||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
})
|
||||
|
||||
const Presets = {
|
||||
fade: {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
transition: smoothPreset,
|
||||
},
|
||||
}
|
||||
export const IconTransition = (
|
||||
props: React.PropsWithChildren<{
|
||||
animatedKey: string
|
||||
initial?: Target
|
||||
animate?: Target
|
||||
exit?: Target
|
||||
transition?: Spring
|
||||
|
||||
preset?: "fade"
|
||||
}>,
|
||||
) => {
|
||||
const preset = Presets[props.preset ?? "fade"]
|
||||
return (
|
||||
<AnimatePresence mode="popLayout">
|
||||
<m.span
|
||||
key={props.animatedKey}
|
||||
initial={props.initial ?? preset.initial}
|
||||
animate={props.animate ?? preset.animate}
|
||||
exit={props.exit ?? preset.exit}
|
||||
transition={props.transition ?? preset.transition}
|
||||
>
|
||||
{props.children}
|
||||
</m.span>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.js"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useGeneralSettingKey } from "~/atoms/settings/general"
|
||||
import { IconTransition } from "~/components/ux/transition/icon"
|
||||
|
||||
export const EnhancedSettingsIndicator = () => {
|
||||
const enhancedSettings = useGeneralSettingKey("enhancedSettings")
|
||||
const { t } = useTranslation("settings")
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<IconTransition animatedKey={enhancedSettings ? "done" : "init"} preset="fade">
|
||||
{enhancedSettings ? (
|
||||
<i className="i-mgc-rocket-cute-fi text-accent size-4" />
|
||||
) : (
|
||||
<i className="i-mgc-rocket-cute-re size-4 opacity-50" />
|
||||
)}
|
||||
</IconTransition>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[40ch]">
|
||||
{enhancedSettings ? (
|
||||
<p>{t("general.enhanced.enabled.tip")}</p>
|
||||
) : (
|
||||
<p>{t("general.enhanced.disabled.tip")}</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ export const SettingSyncIndicator = () => {
|
|||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="center absolute right-2 size-5">
|
||||
<div className="size-5">
|
||||
<metaInfo.icon className="size-4" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
/* eslint-disable @eslint-react/no-array-index-key */
|
||||
import { isNil } from "es-toolkit/compat"
|
||||
import type { FC, ReactNode } from "react"
|
||||
import * as React from "react"
|
||||
import { isValidElement } from "react"
|
||||
|
|
@ -54,68 +53,79 @@ export const createSettingBuilder =
|
|||
const { settings } = props
|
||||
const settingObject = useSetting()
|
||||
|
||||
return settings
|
||||
.filter((i) => !isNil(i))
|
||||
.map((setting, index) => {
|
||||
if (isValidElement(setting)) return setting
|
||||
if (typeof setting === "function") {
|
||||
return React.createElement(setting, { key: index })
|
||||
}
|
||||
const assertSetting = setting as SettingItem<T> | SectionSettingItem | ActionSettingItem
|
||||
const filteredSettings = settings.filter((i) => !!i)
|
||||
return filteredSettings.map((setting, index) => {
|
||||
if (isValidElement(setting)) return setting
|
||||
if (typeof setting === "function") {
|
||||
return React.createElement(setting, { key: index })
|
||||
}
|
||||
const assertSetting = setting as SettingItem<T> | SectionSettingItem | ActionSettingItem
|
||||
|
||||
if (!assertSetting) return null
|
||||
if (assertSetting.disabled) return null
|
||||
if (!assertSetting) return null
|
||||
if (assertSetting.disabled) return null
|
||||
|
||||
if ("type" in assertSetting && assertSetting.type === "title" && assertSetting.value) {
|
||||
return <SettingSectionTitle key={index} title={assertSetting.value} />
|
||||
}
|
||||
if ("type" in assertSetting && assertSetting.type === "title") {
|
||||
return null
|
||||
}
|
||||
const nextItem = filteredSettings[index + 1]
|
||||
// If has no next item or next item is also a title, then it is an empty section
|
||||
const isEmptySection =
|
||||
!nextItem ||
|
||||
(typeof nextItem === "object" && "type" in nextItem && nextItem.type === "title")
|
||||
|
||||
let ControlElement: React.ReactNode
|
||||
const isValidTitle =
|
||||
"type" in assertSetting &&
|
||||
assertSetting.type === "title" &&
|
||||
assertSetting.value &&
|
||||
!isEmptySection
|
||||
|
||||
if ("key" in assertSetting) {
|
||||
switch (typeof settingObject[assertSetting.key]) {
|
||||
case "boolean": {
|
||||
ControlElement = (
|
||||
<SettingSwitch
|
||||
className="mt-4"
|
||||
checked={settingObject[assertSetting.key] as boolean}
|
||||
onCheckedChange={(checked) => assertSetting.onChange(checked as T[keyof T])}
|
||||
label={assertSetting.label}
|
||||
/>
|
||||
)
|
||||
break
|
||||
}
|
||||
case "string": {
|
||||
ControlElement = (
|
||||
<SettingInput
|
||||
vertical={assertSetting.vertical}
|
||||
labelClassName={assertSetting.componentProps?.labelClassName}
|
||||
type={assertSetting.type || "text"}
|
||||
className="mt-4"
|
||||
value={settingObject[assertSetting.key] as string}
|
||||
onChange={(event) => assertSetting.onChange(event.target.value as T[keyof T])}
|
||||
label={assertSetting.label}
|
||||
/>
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
if (isValidTitle) {
|
||||
return <SettingSectionTitle key={index} title={assertSetting.value} />
|
||||
}
|
||||
if ("type" in assertSetting && assertSetting.type === "title") {
|
||||
return null
|
||||
}
|
||||
|
||||
let ControlElement: React.ReactNode
|
||||
|
||||
if ("key" in assertSetting) {
|
||||
switch (typeof settingObject[assertSetting.key]) {
|
||||
case "boolean": {
|
||||
ControlElement = (
|
||||
<SettingSwitch
|
||||
className="mt-4"
|
||||
checked={settingObject[assertSetting.key] as boolean}
|
||||
onCheckedChange={(checked) => assertSetting.onChange(checked as T[keyof T])}
|
||||
label={assertSetting.label}
|
||||
/>
|
||||
)
|
||||
break
|
||||
}
|
||||
case "string": {
|
||||
ControlElement = (
|
||||
<SettingInput
|
||||
vertical={assertSetting.vertical}
|
||||
labelClassName={assertSetting.componentProps?.labelClassName}
|
||||
type={assertSetting.type || "text"}
|
||||
className="mt-4"
|
||||
value={settingObject[assertSetting.key] as string}
|
||||
onChange={(event) => assertSetting.onChange(event.target.value as T[keyof T])}
|
||||
label={assertSetting.label}
|
||||
/>
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
} else if ("action" in assertSetting) {
|
||||
ControlElement = <SettingActionItem {...assertSetting} key={index} />
|
||||
}
|
||||
return (
|
||||
<SettingItemGroup key={index}>
|
||||
{ControlElement}
|
||||
{!!assertSetting.description && (
|
||||
<SettingDescription>{assertSetting.description}</SettingDescription>
|
||||
)}
|
||||
</SettingItemGroup>
|
||||
)
|
||||
})
|
||||
} else if ("action" in assertSetting) {
|
||||
ControlElement = <SettingActionItem {...assertSetting} key={index} />
|
||||
}
|
||||
return (
|
||||
<SettingItemGroup key={index}>
|
||||
{ControlElement}
|
||||
{!!assertSetting.description && (
|
||||
<SettingDescription>{assertSetting.description}</SettingDescription>
|
||||
)}
|
||||
</SettingItemGroup>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { GeneralSettings, UISettings } from "@follow/shared/interface/settings"
|
||||
import type { GeneralSettings, UISettings } from "@follow/shared/settings/interface"
|
||||
import { EventBus } from "@follow/utils/event-bus"
|
||||
import { getStorageNS } from "@follow/utils/ns"
|
||||
import { isEmptyObject, sleep } from "@follow/utils/utils"
|
||||
|
|
@ -34,7 +34,7 @@ const createInternalSetter =
|
|||
jotaiStore.set(atom, { ...current, ...payload })
|
||||
}
|
||||
|
||||
const localsettingSetterMap = {
|
||||
const localSettingSetterMap = {
|
||||
appearance: createInternalSetter(__uiSettingAtom),
|
||||
general: createInternalSetter(__generalSettingAtom),
|
||||
}
|
||||
|
|
@ -249,7 +249,7 @@ class SettingSyncQueue {
|
|||
continue
|
||||
}
|
||||
|
||||
const setter = localsettingSetterMap[tab]
|
||||
const setter = localSettingSetterMap[tab]
|
||||
|
||||
setter(nextPayload)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { defaultSettings } from "@follow/shared/settings/defaults"
|
||||
import { enhancedSettingKeys } from "@follow/shared/settings/enhanced"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import { useGeneralSettingKey } from "~/atoms/settings/general"
|
||||
|
||||
export enum WrapEnhancedSettingTab {
|
||||
General,
|
||||
Appearance,
|
||||
}
|
||||
|
||||
const enhancedSettingMapper: Record<WrapEnhancedSettingTab, Set<keyof any>> = {
|
||||
[WrapEnhancedSettingTab.General]: enhancedSettingKeys.general,
|
||||
[WrapEnhancedSettingTab.Appearance]: enhancedSettingKeys.ui,
|
||||
}
|
||||
const defaultSettingMapper: Record<WrapEnhancedSettingTab, Record<keyof any, any>> = {
|
||||
[WrapEnhancedSettingTab.General]: defaultSettings.general,
|
||||
[WrapEnhancedSettingTab.Appearance]: defaultSettings.ui,
|
||||
}
|
||||
export const useWrapEnhancedSettingItem = <T extends (key: any, options: any) => any>(
|
||||
fn: T,
|
||||
tab: WrapEnhancedSettingTab,
|
||||
): T => {
|
||||
const enableEnhancedSettings = useGeneralSettingKey("enhancedSettings")
|
||||
return useCallback(
|
||||
(key: string, options: any) => {
|
||||
const enhancedKeys = enhancedSettingMapper[tab]
|
||||
const defaults = defaultSettingMapper[tab]
|
||||
|
||||
if (!enhancedKeys || !defaults) {
|
||||
return fn(key, options)
|
||||
}
|
||||
|
||||
if (enhancedKeys.has(key) && !enableEnhancedSettings) {
|
||||
return null
|
||||
}
|
||||
|
||||
return fn(key, options)
|
||||
},
|
||||
[enableEnhancedSettings, fn, tab],
|
||||
) as any as T
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT } from "~/constants"
|
|||
import { useActivationModal } from "~/modules/activation"
|
||||
|
||||
import { SETTING_MODAL_ID } from "../constants"
|
||||
import { EnhancedSettingsIndicator } from "../helper/EnhancedIndicator"
|
||||
import { SettingSyncIndicator } from "../helper/SyncIndicator"
|
||||
import { useAvailableSettings, useSettingPageContext } from "../hooks/use-setting-ctx"
|
||||
import { SettingsSidebarTitle } from "../title"
|
||||
|
|
@ -138,7 +139,8 @@ export function SettingModalLayout(
|
|||
<SidebarItems />
|
||||
</nav>
|
||||
|
||||
<div className="relative -mb-5 h-8 shrink-0">
|
||||
<div className="relative -mb-6 flex h-8 shrink-0 items-center justify-end gap-2">
|
||||
<EnhancedSettingsIndicator />
|
||||
<SettingSyncIndicator />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -41,15 +41,20 @@ import {
|
|||
} from "../control"
|
||||
import { createDefineSettingItem } from "../helper/builder"
|
||||
import { createSettingBuilder } from "../helper/setting-builder"
|
||||
import {
|
||||
useWrapEnhancedSettingItem,
|
||||
WrapEnhancedSettingTab,
|
||||
} from "../hooks/useWrapEnhancedSettingItem"
|
||||
import { SettingItemGroup } from "../section"
|
||||
import { ContentFontSelector, UIFontSelector } from "../sections/fonts"
|
||||
|
||||
const SettingBuilder = createSettingBuilder(useUISettingValue)
|
||||
const defineItem = createDefineSettingItem(useUISettingValue, setUISetting)
|
||||
const _defineItem = createDefineSettingItem(useUISettingValue, setUISetting)
|
||||
|
||||
export const SettingAppearance = () => {
|
||||
const { t } = useTranslation("settings")
|
||||
const isMobile = useMobile()
|
||||
const defineItem = useWrapEnhancedSettingItem(_defineItem, WrapEnhancedSettingTab.Appearance)
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<SettingBuilder
|
||||
|
|
@ -95,7 +100,6 @@ export const SettingAppearance = () => {
|
|||
{
|
||||
type: "title",
|
||||
value: t("appearance.fonts"),
|
||||
disabled: isMobile,
|
||||
},
|
||||
!isMobile && UIFontSelector,
|
||||
!isMobile && TextSize,
|
||||
|
|
|
|||
|
|
@ -28,9 +28,13 @@ import { setTranslationCache } from "~/modules/entry-content/atoms"
|
|||
|
||||
import { SettingDescription, SettingInput, SettingSwitch } from "../control"
|
||||
import { createSetting } from "../helper/builder"
|
||||
import {
|
||||
useWrapEnhancedSettingItem,
|
||||
WrapEnhancedSettingTab,
|
||||
} from "../hooks/useWrapEnhancedSettingItem"
|
||||
import { SettingItemGroup } from "../section"
|
||||
|
||||
const { defineSettingItem, SettingBuilder } = createSetting(
|
||||
const { defineSettingItem: _defineSettingItem, SettingBuilder } = createSetting(
|
||||
useGeneralSettingValue,
|
||||
setGeneralSetting,
|
||||
)
|
||||
|
|
@ -48,11 +52,19 @@ export const SettingGeneral = () => {
|
|||
setGeneralSetting("appLaunchOnStartup", checked)
|
||||
}, [])
|
||||
|
||||
const defineSettingItem = useWrapEnhancedSettingItem(
|
||||
_defineSettingItem,
|
||||
WrapEnhancedSettingTab.General,
|
||||
)
|
||||
|
||||
const isMobile = useMobile()
|
||||
|
||||
const reRenderKey = useGeneralSettingKey("enhancedSettings")
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<SettingBuilder
|
||||
key={reRenderKey.toString()}
|
||||
settings={[
|
||||
{
|
||||
type: "title",
|
||||
|
|
@ -119,12 +131,18 @@ export const SettingGeneral = () => {
|
|||
description: t("general.mark_as_read.render.description"),
|
||||
}),
|
||||
|
||||
{ type: "title", value: "TTS", disabled: !IN_ELECTRON },
|
||||
{ type: "title", value: "TTS" },
|
||||
|
||||
IN_ELECTRON && VoiceSelector,
|
||||
|
||||
{ type: "title", value: t("general.network"), disabled: !IN_ELECTRON },
|
||||
{ type: "title", value: t("general.network") },
|
||||
IN_ELECTRON && NettingSetting,
|
||||
|
||||
{ type: "title", value: t("general.advanced") },
|
||||
defineSettingItem("enhancedSettings", {
|
||||
label: t("general.enhanced.label"),
|
||||
description: t("general.enhanced.description"),
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { GeneralSettings } from "@follow/shared/interface/settings"
|
||||
import type { GeneralSettings } from "@follow/shared/settings/interface"
|
||||
|
||||
import { createSettingAtom } from "./helper"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import type { GeneralSettings, UISettings } from "@follow/shared/settings/interface"
|
||||
import type { BrowserWindow } from "electron"
|
||||
import { useEffect, useLayoutEffect, useRef } from "react"
|
||||
import type { toast } from "sonner"
|
||||
|
||||
import type { GeneralSettings, UISettings } from "./interface/settings"
|
||||
|
||||
const PREFIX = "__follow"
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
import type { GeneralSettings, IntegrationSettings, UISettings } from "./interface"
|
||||
|
||||
export const defaultGeneralSettings: GeneralSettings = {
|
||||
// App
|
||||
appLaunchOnStartup: false,
|
||||
language: "en",
|
||||
actionLanguage: "default",
|
||||
|
||||
// mobile app
|
||||
startupScreen: "timeline",
|
||||
// Data control
|
||||
dataPersist: true,
|
||||
sendAnonymousData: true,
|
||||
showQuickTimeline: true,
|
||||
|
||||
autoGroup: true,
|
||||
|
||||
// view
|
||||
unreadOnly: true,
|
||||
// mark unread
|
||||
scrollMarkUnread: true,
|
||||
hoverMarkUnread: true,
|
||||
renderMarkUnread: false,
|
||||
// UX
|
||||
groupByDate: true,
|
||||
autoExpandLongSocialMedia: false,
|
||||
|
||||
// Secure
|
||||
jumpOutLinkWarn: true,
|
||||
// TTS
|
||||
voice: "en-US-AndrewMultilingualNeural",
|
||||
|
||||
// Pro feature
|
||||
enhancedSettings: false,
|
||||
}
|
||||
|
||||
export const defaultUISettings: UISettings = {
|
||||
// Sidebar
|
||||
entryColWidth: 356,
|
||||
feedColWidth: 256,
|
||||
hideExtraBadge: false,
|
||||
|
||||
opaqueSidebar: false,
|
||||
sidebarShowUnreadCount: true,
|
||||
thumbnailRatio: "square",
|
||||
|
||||
// Global UI
|
||||
uiTextSize: 16,
|
||||
// System
|
||||
showDockBadge: true,
|
||||
// Misc
|
||||
modalOverlay: true,
|
||||
modalDraggable: true,
|
||||
modalOpaque: true,
|
||||
reduceMotion: false,
|
||||
usePointerCursor: false,
|
||||
|
||||
// Font
|
||||
uiFontFamily: "SN Pro",
|
||||
readerFontFamily: "inherit",
|
||||
contentFontSize: 16,
|
||||
dateFormat: "default",
|
||||
contentLineHeight: 1.75,
|
||||
// Content
|
||||
readerRenderInlineStyle: true,
|
||||
codeHighlightThemeLight: "github-light",
|
||||
codeHighlightThemeDark: "github-dark",
|
||||
guessCodeLanguage: true,
|
||||
hideRecentReader: false,
|
||||
customCSS: "",
|
||||
|
||||
// View
|
||||
pictureViewMasonry: true,
|
||||
wideMode: false,
|
||||
|
||||
// Action Order
|
||||
toolbarOrder: {
|
||||
main: [],
|
||||
more: [],
|
||||
},
|
||||
}
|
||||
|
||||
export const defaultIntegrationSettings: IntegrationSettings = {
|
||||
// eagle
|
||||
enableEagle: true,
|
||||
|
||||
// readwise
|
||||
enableReadwise: false,
|
||||
readwiseToken: "",
|
||||
|
||||
// instapaper
|
||||
enableInstapaper: false,
|
||||
instapaperUsername: "",
|
||||
instapaperPassword: "",
|
||||
|
||||
// obsidian
|
||||
enableObsidian: false,
|
||||
obsidianVaultPath: "",
|
||||
|
||||
// outline
|
||||
enableOutline: false,
|
||||
outlineEndpoint: "",
|
||||
outlineToken: "",
|
||||
outlineCollection: "",
|
||||
|
||||
// readeck
|
||||
enableReadeck: false,
|
||||
readeckEndpoint: "",
|
||||
readeckToken: "",
|
||||
}
|
||||
|
||||
export const defaultSettings = {
|
||||
general: defaultGeneralSettings,
|
||||
ui: defaultUISettings,
|
||||
integration: defaultIntegrationSettings,
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import type { GeneralSettings, UISettings } from "./interface"
|
||||
|
||||
export const enhancedGeneralSettingKeys = new Set<keyof GeneralSettings>([
|
||||
"groupByDate",
|
||||
"autoExpandLongSocialMedia",
|
||||
])
|
||||
export const enhancedUISettingKeys = new Set<keyof UISettings>([
|
||||
"hideExtraBadge",
|
||||
"thumbnailRatio",
|
||||
"codeHighlightThemeLight",
|
||||
"codeHighlightThemeDark",
|
||||
"dateFormat",
|
||||
"readerRenderInlineStyle",
|
||||
"modalOverlay",
|
||||
"reduceMotion",
|
||||
"usePointerCursor",
|
||||
"opaqueSidebar",
|
||||
])
|
||||
|
||||
export const enhancedSettingKeys = {
|
||||
general: enhancedGeneralSettingKeys,
|
||||
ui: enhancedUISettingKeys,
|
||||
}
|
||||
|
|
@ -22,6 +22,9 @@ export interface GeneralSettings {
|
|||
* Auto expand long social media
|
||||
*/
|
||||
autoExpandLongSocialMedia: boolean
|
||||
|
||||
// Pro feature
|
||||
enhancedSettings: boolean
|
||||
}
|
||||
|
||||
export interface UISettings {
|
||||
Loading…
Reference in New Issue