diff --git a/apps/desktop/locales/settings/en.json b/apps/desktop/locales/settings/en.json index 6ebe5e85d..cd704f07a 100644 --- a/apps/desktop/locales/settings/en.json +++ b/apps/desktop/locales/settings/en.json @@ -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." } diff --git a/apps/desktop/src/renderer/src/atoms/settings/general.ts b/apps/desktop/src/renderer/src/atoms/settings/general.ts index f4a28a9e8..48eb9e9f5 100644 --- a/apps/desktop/src/renderer/src/atoms/settings/general.ts +++ b/apps/desktop/src/renderer/src/atoms/settings/general.ts @@ -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, + defaultSettings: Record, +): [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", diff --git a/apps/desktop/src/renderer/src/atoms/settings/integration.ts b/apps/desktop/src/renderer/src/atoms/settings/integration.ts index f8703e3cb..02895ca53 100644 --- a/apps/desktop/src/renderer/src/atoms/settings/integration.ts +++ b/apps/desktop/src/renderer/src/atoms/settings/integration.ts @@ -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, diff --git a/apps/desktop/src/renderer/src/atoms/settings/ui.ts b/apps/desktop/src/renderer/src/atoms/settings/ui.ts index c5433f3b6..cfb79636a 100644 --- a/apps/desktop/src/renderer/src/atoms/settings/ui.ts +++ b/apps/desktop/src/renderer/src/atoms/settings/ui.ts @@ -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", diff --git a/apps/desktop/src/renderer/src/components/ui/constants/spring.ts b/apps/desktop/src/renderer/src/components/ui/constants/spring.ts index 13ac6ee12..f224a44b2 100644 --- a/apps/desktop/src/renderer/src/components/ui/constants/spring.ts +++ b/apps/desktop/src/renderer/src/components/ui/constants/spring.ts @@ -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, + } +} diff --git a/apps/desktop/src/renderer/src/components/ux/transition/icon.tsx b/apps/desktop/src/renderer/src/components/ux/transition/icon.tsx index 982c571ef..882aef604 100644 --- a/apps/desktop/src/renderer/src/components/ux/transition/icon.tsx +++ b/apps/desktop/src/renderer/src/components/ux/transition/icon.tsx @@ -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 ( + + + {props.children} + + + ) +} diff --git a/apps/desktop/src/renderer/src/modules/settings/helper/EnhancedIndicator.tsx b/apps/desktop/src/renderer/src/modules/settings/helper/EnhancedIndicator.tsx new file mode 100644 index 000000000..efabc30fa --- /dev/null +++ b/apps/desktop/src/renderer/src/modules/settings/helper/EnhancedIndicator.tsx @@ -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 ( + + + + {enhancedSettings ? ( + + ) : ( + + )} + + + + {enhancedSettings ? ( +

{t("general.enhanced.enabled.tip")}

+ ) : ( +

{t("general.enhanced.disabled.tip")}

+ )} +
+
+ ) +} diff --git a/apps/desktop/src/renderer/src/modules/settings/helper/SyncIndicator.tsx b/apps/desktop/src/renderer/src/modules/settings/helper/SyncIndicator.tsx index eefb40f0e..97d88b413 100644 --- a/apps/desktop/src/renderer/src/modules/settings/helper/SyncIndicator.tsx +++ b/apps/desktop/src/renderer/src/modules/settings/helper/SyncIndicator.tsx @@ -60,7 +60,7 @@ export const SettingSyncIndicator = () => { return ( -
+
diff --git a/apps/desktop/src/renderer/src/modules/settings/helper/setting-builder.tsx b/apps/desktop/src/renderer/src/modules/settings/helper/setting-builder.tsx index 3ae95bf30..d743fa2cb 100644 --- a/apps/desktop/src/renderer/src/modules/settings/helper/setting-builder.tsx +++ b/apps/desktop/src/renderer/src/modules/settings/helper/setting-builder.tsx @@ -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 | 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 | 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 - } - 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 = ( - assertSetting.onChange(checked as T[keyof T])} - label={assertSetting.label} - /> - ) - break - } - case "string": { - ControlElement = ( - assertSetting.onChange(event.target.value as T[keyof T])} - label={assertSetting.label} - /> - ) - break - } - default: { - return null - } + if (isValidTitle) { + return + } + 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 = ( + assertSetting.onChange(checked as T[keyof T])} + label={assertSetting.label} + /> + ) + break + } + case "string": { + ControlElement = ( + assertSetting.onChange(event.target.value as T[keyof T])} + label={assertSetting.label} + /> + ) + break + } + default: { + return null } - } else if ("action" in assertSetting) { - ControlElement = } - return ( - - {ControlElement} - {!!assertSetting.description && ( - {assertSetting.description} - )} - - ) - }) + } else if ("action" in assertSetting) { + ControlElement = + } + return ( + + {ControlElement} + {!!assertSetting.description && ( + {assertSetting.description} + )} + + ) + }) } diff --git a/apps/desktop/src/renderer/src/modules/settings/helper/sync-queue.ts b/apps/desktop/src/renderer/src/modules/settings/helper/sync-queue.ts index c3398b9d3..2df5b388c 100644 --- a/apps/desktop/src/renderer/src/modules/settings/helper/sync-queue.ts +++ b/apps/desktop/src/renderer/src/modules/settings/helper/sync-queue.ts @@ -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) } diff --git a/apps/desktop/src/renderer/src/modules/settings/hooks/useWrapEnhancedSettingItem.ts b/apps/desktop/src/renderer/src/modules/settings/hooks/useWrapEnhancedSettingItem.ts new file mode 100644 index 000000000..a8322bdda --- /dev/null +++ b/apps/desktop/src/renderer/src/modules/settings/hooks/useWrapEnhancedSettingItem.ts @@ -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.General]: enhancedSettingKeys.general, + [WrapEnhancedSettingTab.Appearance]: enhancedSettingKeys.ui, +} +const defaultSettingMapper: Record> = { + [WrapEnhancedSettingTab.General]: defaultSettings.general, + [WrapEnhancedSettingTab.Appearance]: defaultSettings.ui, +} +export const useWrapEnhancedSettingItem = 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 +} diff --git a/apps/desktop/src/renderer/src/modules/settings/modal/layout.tsx b/apps/desktop/src/renderer/src/modules/settings/modal/layout.tsx index 81cafad1e..0f58f4d98 100644 --- a/apps/desktop/src/renderer/src/modules/settings/modal/layout.tsx +++ b/apps/desktop/src/renderer/src/modules/settings/modal/layout.tsx @@ -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( -
+
+
diff --git a/apps/desktop/src/renderer/src/modules/settings/tabs/apperance.tsx b/apps/desktop/src/renderer/src/modules/settings/tabs/apperance.tsx index 5ce13ed51..b56e5610c 100644 --- a/apps/desktop/src/renderer/src/modules/settings/tabs/apperance.tsx +++ b/apps/desktop/src/renderer/src/modules/settings/tabs/apperance.tsx @@ -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 (
{ { type: "title", value: t("appearance.fonts"), - disabled: isMobile, }, !isMobile && UIFontSelector, !isMobile && TextSize, diff --git a/apps/desktop/src/renderer/src/modules/settings/tabs/general.tsx b/apps/desktop/src/renderer/src/modules/settings/tabs/general.tsx index 7e3394b65..1d1148bee 100644 --- a/apps/desktop/src/renderer/src/modules/settings/tabs/general.tsx +++ b/apps/desktop/src/renderer/src/modules/settings/tabs/general.tsx @@ -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 (
{ 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"), + }), ]} />
diff --git a/apps/ssr/client/atoms/settings/general.ts b/apps/ssr/client/atoms/settings/general.ts index d81c273a2..f5317b701 100644 --- a/apps/ssr/client/atoms/settings/general.ts +++ b/apps/ssr/client/atoms/settings/general.ts @@ -1,4 +1,4 @@ -import type { GeneralSettings } from "@follow/shared/interface/settings" +import type { GeneralSettings } from "@follow/shared/settings/interface" import { createSettingAtom } from "./helper" diff --git a/packages/shared/src/bridge.ts b/packages/shared/src/bridge.ts index 5e7b52ca1..017fbd4a8 100644 --- a/packages/shared/src/bridge.ts +++ b/packages/shared/src/bridge.ts @@ -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 diff --git a/packages/shared/src/settings/defaults.ts b/packages/shared/src/settings/defaults.ts new file mode 100644 index 000000000..6ba6aa1da --- /dev/null +++ b/packages/shared/src/settings/defaults.ts @@ -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, +} diff --git a/packages/shared/src/settings/enhanced.ts b/packages/shared/src/settings/enhanced.ts new file mode 100644 index 000000000..f6562635b --- /dev/null +++ b/packages/shared/src/settings/enhanced.ts @@ -0,0 +1,23 @@ +import type { GeneralSettings, UISettings } from "./interface" + +export const enhancedGeneralSettingKeys = new Set([ + "groupByDate", + "autoExpandLongSocialMedia", +]) +export const enhancedUISettingKeys = new Set([ + "hideExtraBadge", + "thumbnailRatio", + "codeHighlightThemeLight", + "codeHighlightThemeDark", + "dateFormat", + "readerRenderInlineStyle", + "modalOverlay", + "reduceMotion", + "usePointerCursor", + "opaqueSidebar", +]) + +export const enhancedSettingKeys = { + general: enhancedGeneralSettingKeys, + ui: enhancedUISettingKeys, +} diff --git a/packages/shared/src/interface/settings.ts b/packages/shared/src/settings/interface.ts similarity index 97% rename from packages/shared/src/interface/settings.ts rename to packages/shared/src/settings/interface.ts index f1f8f9bf9..a0d9c5528 100644 --- a/packages/shared/src/interface/settings.ts +++ b/packages/shared/src/settings/interface.ts @@ -22,6 +22,9 @@ export interface GeneralSettings { * Auto expand long social media */ autoExpandLongSocialMedia: boolean + + // Pro feature + enhancedSettings: boolean } export interface UISettings {