feat(desktop): simplify settings via enhance settings toggle

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-03-26 23:20:32 +08:00
parent 92f76b93d5
commit 217e1a8f2a
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
19 changed files with 626 additions and 187 deletions

View File

@ -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."
}

View File

@ -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",

View File

@ -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,

View File

@ -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",

View File

@ -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,
}
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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>
)
})
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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>

View File

@ -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,

View File

@ -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>

View File

@ -1,4 +1,4 @@
import type { GeneralSettings } from "@follow/shared/interface/settings"
import type { GeneralSettings } from "@follow/shared/settings/interface"
import { createSettingAtom } from "./helper"

View File

@ -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

View File

@ -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,
}

View File

@ -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,
}

View File

@ -22,6 +22,9 @@ export interface GeneralSettings {
* Auto expand long social media
*/
autoExpandLongSocialMedia: boolean
// Pro feature
enhancedSettings: boolean
}
export interface UISettings {