feat(app): support cache limit and clean cache

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2024-11-05 23:00:34 +08:00
parent 1d0eca1ca1
commit dee294dda5
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
48 changed files with 624 additions and 128 deletions

View File

@ -34,6 +34,7 @@
"electron-log": "5.2.0",
"electron-squirrel-startup": "1.0.1",
"electron-updater": "^6.3.9",
"fast-folder-size": "2.3.0",
"font-list": "1.5.1",
"i18next": "^23.16.4",
"linkedom": "^0.18.5",

View File

@ -9,6 +9,7 @@ import { app, nativeTheme, Notification, shell } from "electron"
import contextMenu from "electron-context-menu"
import { getIconPath } from "./helper"
import { clearCacheCronJob } from "./lib/cleaner"
import { t } from "./lib/i18n"
import { store } from "./lib/store"
import { updateNotificationsToken } from "./lib/user"
@ -59,6 +60,7 @@ export const initializeAppStage1 = () => {
registerMenuAndContextMenu()
registerPushNotifications()
clearCacheCronJob()
}
let contextMenuDisposer: () => void

View File

@ -1,9 +1,20 @@
import { callWindowExpose } from "@follow/shared/bridge"
import { dialog } from "electron"
import { statSync } from "node:fs"
import fsp from "node:fs/promises"
import { createRequire } from "node:module"
import path from "node:path"
import { promisify } from "node:util"
import { callWindowExpose } from "@follow/shared/bridge"
import { app, dialog } from "electron"
import { logger } from "~/logger"
import { getMainWindow } from "~/window"
import { t } from "./i18n"
import { store, StoreKey } from "./store"
const require = createRequire(import.meta.url)
const fastFolderSize = require("fast-folder-size") as any as typeof import("fast-folder-size")
export const clearAllDataAndConfirm = async () => {
const win = getMainWindow()
@ -53,3 +64,75 @@ export const clearAllData = async () => {
caller.toast.error(`Error resetting app data: ${error.message}`)
}
}
const fastFolderSizeAsync = promisify(fastFolderSize)
export const getCacheSize = async () => {
const cachePath = path.join(app.getPath("userData"), "cache")
// Size is in bytes
const sizeInBytes = await fastFolderSizeAsync(cachePath)
return sizeInBytes || 0
}
const getCachedFilesRecursive = async (dir: string, result: string[] = []) => {
const files = await fsp.readdir(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stat = await fsp.stat(filePath)
if (stat.isDirectory()) {
const files = await getCachedFilesRecursive(filePath)
result.push(...files)
} else {
result.push(filePath)
}
}
return result
}
let timer: any = null
export const clearCacheCronJob = () => {
if (timer) {
timer = clearInterval(timer)
}
timer = setInterval(
async () => {
const hasLimit = store.get(StoreKey.CacheSizeLimit)
if (!hasLimit) {
return
}
const cacheSize = await getCacheSize()
const limitByteSize = hasLimit * 1024 * 1024
if (cacheSize > limitByteSize) {
const shouldCleanSize = cacheSize - limitByteSize - 1024 * 1024 * 50 // 50MB
const cachePath = path.join(app.getPath("userData"), "cache")
const files = await getCachedFilesRecursive(cachePath)
// Sort by last modified
files.sort((a, b) => {
const aStat = statSync(a)
const bStat = statSync(b)
return bStat.mtime.getTime() - aStat.mtime.getTime()
})
let cleanedSize = 0
for (const file of files) {
const fileSize = statSync(file).size
cleanedSize += fileSize
if (cleanedSize >= shouldCleanSize) {
logger.info(`Cleaned ${cleanedSize} bytes cache`)
break
}
}
}
},
10 * 60 * 1000,
) // 10 min
return () => {
if (!timer) return
timer = clearInterval(timer)
}
}

View File

@ -15,6 +15,11 @@ const createOrGetDb = () => {
}
return db
}
export enum StoreKey {
CacheSizeLimit = "cacheSizeLimit",
}
export const store = {
get: (key: string) => {
const db = createOrGetDb()
@ -26,4 +31,9 @@ export const store = {
db.data[key] = value
db.write()
},
delete: (key: string) => {
const db = createOrGetDb()
delete db.data[key]
db.write()
},
}

View File

@ -8,7 +8,9 @@ import type { BrowserWindow } from "electron"
import { app, clipboard, dialog, screen } from "electron"
import { registerMenuAndContextMenu } from "~/init"
import { clearAllData } from "~/lib/cleaner"
import { clearAllData, getCacheSize } from "~/lib/cleaner"
import { store, StoreKey } from "~/lib/store"
import { logger } from "~/logger"
import { isWindows11 } from "../env"
import { downloadFile } from "../lib/download"
@ -267,6 +269,27 @@ ${content}
return { success: false, error: errorMessage }
}
}),
getCacheSize: t.procedure.action(async () => {
return getCacheSize()
}),
getCacheLimit: t.procedure.action(async () => {
return store.get(StoreKey.CacheSizeLimit)
}),
clearCache: t.procedure.action(async () => {
const cachePath = path.join(app.getPath("userData"), "cache")
await fsp.rm(cachePath, { recursive: true, force: true })
}),
limitCacheSize: t.procedure.input<number>().action(async ({ input }) => {
logger.info("set limitCacheSize", input)
if (input === 0) {
store.delete(StoreKey.CacheSizeLimit)
} else {
store.set(StoreKey.CacheSizeLimit, input)
}
}),
}
interface Sender extends Electron.WebContents {

View File

@ -5,7 +5,7 @@ function getSettings() {
const settings = [] as {
name: I18nKeysForSettings
iconName: string
icon: string | React.ReactNode
path: string
Component: () => JSX.Element
priority: number

View File

@ -0,0 +1,192 @@
import { CarbonInfinitySymbol } from "@follow/components/icons/infinify.jsx"
import { Button } from "@follow/components/ui/button/index.js"
import { Label } from "@follow/components/ui/label/index.jsx"
import { Slider } from "@follow/components/ui/slider/index.js"
import { env } from "@follow/shared/env"
import { useQuery } from "@tanstack/react-query"
import { useEffect } from "react"
import { useTranslation } from "react-i18next"
import { setGeneralSetting, useGeneralSettingValue } from "~/atoms/settings/general"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { exportDB } from "~/database"
import { initAnalytics } from "~/initialize/analytics"
import { tipcClient } from "~/lib/client"
import { queryClient } from "~/lib/query-client"
import { clearLocalPersistStoreData } from "~/store/utils/clear"
import { SettingDescription } from "../control"
import { createSetting } from "../helper/builder"
import { SettingItemGroup } from "../section"
const { defineSettingItem, SettingBuilder } = createSetting(
useGeneralSettingValue,
setGeneralSetting,
)
export const SettingDataControl = () => {
const { t } = useTranslation("settings")
useEffect(() => {
tipcClient?.getLoginItemSettings().then((settings) => {
setGeneralSetting("appLaunchOnStartup", settings.openAtLogin)
})
}, [])
const { present } = useModalStack()
return (
<div className="mt-4">
<SettingBuilder
settings={[
{
type: "title",
value: t("general.privacy"),
},
defineSettingItem("sendAnonymousData", {
label: t("general.send_anonymous_data.label"),
description: t("general.send_anonymous_data.description"),
onChange(value) {
setGeneralSetting("sendAnonymousData", value)
if (value) {
initAnalytics()
} else {
window.analytics?.reset()
delete window.analytics
}
},
}),
{
type: "title",
value: t("general.data"),
},
defineSettingItem("dataPersist", {
label: t("general.data_persist.label"),
description: t("general.data_persist.description"),
}),
{
label: t("general.rebuild_database.label"),
action: () => {
present({
title: t("general.rebuild_database.title"),
clickOutsideToDismiss: true,
content: () => (
<div className="text-sm">
<p>{t("general.rebuild_database.warning.line1")}</p>
<p>{t("general.rebuild_database.warning.line2")}</p>
<div className="mt-4 flex justify-end">
<Button
className="bg-red-500 px-3 text-white"
onClick={async () => {
await clearLocalPersistStoreData()
window.location.reload()
}}
>
{t("ok", { ns: "common" })}
</Button>
</div>
</div>
),
})
},
description: t("general.rebuild_database.description"),
buttonText: t("general.rebuild_database.button"),
},
{
label: t("general.export_database.label"),
description: t("general.export_database.description"),
buttonText: t("general.export_database.button"),
action: () => {
exportDB()
},
},
{
label: t("general.export.label"),
description: t("general.export.description"),
buttonText: t("general.export.button"),
action: () => {
const link = document.createElement("a")
link.href = `${env.VITE_API_URL}/subscriptions/export`
link.download = "follow.opml"
link.click()
},
},
{
type: "title",
value: t("general.cache"),
},
AppCacheLimit,
{
label: t("data_control.clean_cache.button"),
description: t("data_control.clean_cache.description"),
buttonText: t("data_control.clean_cache.button"),
action: async () => {
await tipcClient?.clearCache()
queryClient.invalidateQueries({ queryKey: ["app", "cache", "size"] })
},
},
]}
/>
</div>
)
}
const AppCacheLimit = () => {
const { t } = useTranslation("settings")
const { data: cacheSize, isLoading: isLoadingCacheSize } = useQuery({
queryKey: ["app", "cache", "size"],
queryFn: async () => {
const byteSize = (await tipcClient?.getCacheSize()) ?? 0
return Math.round(byteSize / 1024 / 1024)
},
})
const {
data: cacheLimit,
isLoading: isLoadingCacheLimit,
refetch: refetchCacheLimit,
} = useQuery({
queryKey: ["app", "cache", "limit"],
queryFn: async () => {
const size = (await tipcClient?.getCacheLimit()) ?? 0
return size
},
})
const onChange = (value: number[]) => {
tipcClient?.limitCacheSize(value[0])
refetchCacheLimit()
}
if (isLoadingCacheSize || isLoadingCacheLimit) return null
const InfinitySymbol = <CarbonInfinitySymbol />
return (
<SettingItemGroup>
<div className={"mb-3 flex items-center justify-between gap-4"}>
<Label className="center flex">
{t("data_control.app_cache_limit.label")}
<span className="center ml-2 flex shrink-0 gap-1 text-xs text-gray-500">
<span>({cacheSize}M</span> /{" "}
<span className="center flex shrink-0">
{cacheLimit ? `${cacheLimit}M` : InfinitySymbol})
</span>
</span>
</Label>
<div className="relative flex w-1/5 flex-col gap-1">
<Slider
min={0}
max={500}
step={100}
defaultValue={[cacheLimit ?? 0]}
onValueCommit={onChange}
/>
<div className="absolute bottom-[-1.5em] text-base opacity-50">{InfinitySymbol}</div>
<div className="absolute bottom-[-1.5em] right-0 text-xs opacity-50">500M</div>
</div>
</div>
<SettingDescription>{t("data_control.app_cache_limit.description")}</SettingDescription>
</SettingItemGroup>
)
}

View File

@ -1,4 +1,3 @@
import { Button } from "@follow/components/ui/button/index.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import {
Select,
@ -8,7 +7,6 @@ import {
SelectValue,
} from "@follow/components/ui/select/index.jsx"
import { IN_ELECTRON } from "@follow/shared/constants"
import { env } from "@follow/shared/env"
import { cn } from "@follow/utils/utils"
import { useQuery } from "@tanstack/react-query"
import { useAtom } from "jotai"
@ -24,13 +22,9 @@ import {
useGeneralSettingSelector,
useGeneralSettingValue,
} from "~/atoms/settings/general"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { exportDB } from "~/database"
import { useProxyValue, useSetProxy } from "~/hooks/biz/useProxySetting"
import { fallbackLanguage } from "~/i18n"
import { initAnalytics } from "~/initialize/analytics"
import { tipcClient } from "~/lib/client"
import { clearLocalPersistStoreData } from "~/store/utils/clear"
import { SettingDescription, SettingInput } from "../control"
import { createSetting } from "../helper/builder"
@ -54,8 +48,6 @@ export const SettingGeneral = () => {
setGeneralSetting("appLaunchOnStartup", checked)
}, [])
const { present } = useModalStack()
return (
<div className="mt-4">
<SettingBuilder
@ -105,82 +97,6 @@ export const SettingGeneral = () => {
IN_ELECTRON && VoiceSelector,
// { type: "title", value: "Secure" },
// defineSettingItem("jumpOutLinkWarn", {
// label: "Warn when opening external links",
// description: "When you open an untrusted external link, you need to make sure that you open the link.",
// }),
{
type: "title",
value: t("general.privacy_data"),
},
defineSettingItem("dataPersist", {
label: t("general.data_persist.label"),
description: t("general.data_persist.description"),
}),
defineSettingItem("sendAnonymousData", {
label: t("general.send_anonymous_data.label"),
description: t("general.send_anonymous_data.description"),
onChange(value) {
setGeneralSetting("sendAnonymousData", value)
if (value) {
initAnalytics()
} else {
window.analytics?.reset()
delete window.analytics
}
},
}),
{
label: t("general.rebuild_database.label"),
action: () => {
present({
title: t("general.rebuild_database.title"),
clickOutsideToDismiss: true,
content: () => (
<div className="text-sm">
<p>{t("general.rebuild_database.warning.line1")}</p>
<p>{t("general.rebuild_database.warning.line2")}</p>
<div className="mt-4 flex justify-end">
<Button
className="bg-red-500 px-3 text-white"
onClick={async () => {
await clearLocalPersistStoreData()
window.location.reload()
}}
>
{t("ok", { ns: "common" })}
</Button>
</div>
</div>
),
})
},
description: t("general.rebuild_database.description"),
buttonText: t("general.rebuild_database.button"),
},
{
label: t("general.export_database.label"),
description: t("general.export_database.description"),
buttonText: t("general.export_database.button"),
action: () => {
exportDB()
},
},
{
label: t("general.export.label"),
description: t("general.export.description"),
buttonText: t("general.export.button"),
action: () => {
const link = document.createElement("a")
link.href = `${env.VITE_API_URL}/subscriptions/export`
link.download = "follow.opml"
link.click()
},
},
{ type: "title", value: t("general.network"), disabled: !IN_ELECTRON },
IN_ELECTRON && NettingSetting,
]}
@ -189,7 +105,7 @@ export const SettingGeneral = () => {
)
}
export const VoiceSelector = () => {
const VoiceSelector = () => {
const { t } = useTranslation("settings")
const { data } = useQuery({

View File

@ -1,5 +1,6 @@
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js"
import { cn } from "@follow/utils/utils"
import { Slot } from "@radix-ui/react-slot"
import { useContext } from "react"
import { useTranslation } from "react-i18next"
import { useLoaderData } from "react-router-dom"
@ -18,7 +19,11 @@ export const SettingsSidebarTitle = ({ path, className }: { path: string; classN
return (
<div className={cn("flex min-w-0 items-center gap-2 text-[0.94rem] font-medium", className)}>
<i className={`${tab.iconName} shrink-0 text-[19px]`} />
{typeof tab.icon === "string" ? (
<i className={`${tab.icon} shrink-0 text-[19px]`} />
) : (
<Slot className="shrink-0 text-[19px]">{tab.icon}</Slot>
)}
<EllipsisHorizontalTextWithTooltip>{t(tab.name as any)}</EllipsisHorizontalTextWithTooltip>
</div>
)
@ -33,11 +38,12 @@ export const SettingsTitle = ({
}) => {
const { t } = useTranslation("settings")
const {
iconName,
icon: iconName,
name: title,
headerIcon,
} = (useLoaderData() || loader?.() || {}) as SettingPageConfig
const usedIcon = headerIcon || iconName
const isInSettingIndependentWindow = useContext(IsInSettingIndependentWindowContext)
if (!title) {
return null
@ -51,7 +57,7 @@ export const SettingsTitle = ({
className,
)}
>
<i className={headerIcon || iconName} />
{typeof usedIcon === "string" ? <i className={usedIcon} /> : usedIcon}
<span>{t(title as any)}</span>
</div>
)

View File

@ -10,10 +10,10 @@ export enum DisableWhy {
}
export interface SettingPageConfig {
iconName: string
icon: string | React.ReactNode
name: I18nKeysForSettings
priority: number
headerIcon?: string
headerIcon?: string | React.ReactNode
hideIf?: (ctx: SettingPageContext) => boolean
disableIf?: (ctx: SettingPageContext) => [boolean, DisableWhy]
}

View File

@ -3,7 +3,7 @@ import { SettingsTitle } from "~/modules/settings/title"
import { defineSettingPageData } from "~/modules/settings/utils"
export const loader = defineSettingPageData({
iconName: "i-mgc-information-cute-re",
icon: "i-mgc-information-cute-re",
name: "titles.about",
priority: 9999,
})

View File

@ -8,7 +8,7 @@ const iconName = "i-mgc-magic-2-cute-re"
const priority = 1020
export const loader = defineSettingPageData({
iconName,
icon: iconName,
name: "titles.actions",
priority,
disableIf: (ctx) => [ctx.role === UserRole.Trial, DisableWhy.NotActivation],

View File

@ -6,7 +6,7 @@ const iconName = "i-mgc-palette-cute-re"
const priority = 1010
export const loader = defineSettingPageData({
iconName,
icon: iconName,
name: "titles.appearance",
priority,
})

View File

@ -0,0 +1,22 @@
import { MaterialSymbolsDatabaseOutline } from "@follow/components/icons/Database.js"
import { SettingDataControl } from "~/modules/settings/tabs/data-control"
import { SettingsTitle } from "~/modules/settings/title"
import { defineSettingPageData } from "~/modules/settings/utils"
const priority = 1025
export const loader = defineSettingPageData({
icon: <MaterialSymbolsDatabaseOutline />,
name: "titles.data_control",
priority,
})
export function Component() {
return (
<>
<SettingsTitle />
<SettingDataControl />
</>
)
}

View File

@ -6,7 +6,7 @@ const iconName = "i-mgc-certificate-cute-re"
const priority = 1060
export const loader = defineSettingPageData({
iconName,
icon: iconName,
name: "titles.feeds",
priority,
})

View File

@ -6,7 +6,7 @@ const iconName = "i-mgc-settings-7-cute-re"
const priority = 1000
export const loader = defineSettingPageData({
iconName,
icon: iconName,
name: "titles.general",
priority,
})

View File

@ -6,7 +6,7 @@ const iconName = "i-mgc-department-cute-re"
const priority = 1030
export const loader = defineSettingPageData({
iconName,
icon: iconName,
name: "titles.integration",
priority,
})

View File

@ -8,7 +8,7 @@ const iconName = "i-mgc-love-cute-re"
const priority = 1070
export const loader = defineSettingPageData({
iconName,
icon: iconName,
name: "titles.invitations",
priority,
disableIf: (ctx) => [ctx.role === UserRole.Trial, DisableWhy.NotActivation],

View File

@ -8,7 +8,7 @@ const iconName = "i-mgc-rada-cute-re"
const priority = 1050
export const loader = defineSettingPageData({
iconName,
icon: iconName,
name: "titles.lists",
priority,
disableIf: (ctx) => [ctx.role === UserRole.Trial, DisableWhy.NotActivation],

View File

@ -5,7 +5,7 @@ import { defineSettingPageData } from "~/modules/settings/utils"
const iconName = "i-mgc-user-setting-cute-re"
const priority = 1090
export const loader = defineSettingPageData({
iconName,
icon: iconName,
name: "titles.profile",
priority,
})

View File

@ -10,7 +10,7 @@ const iconName = "i-mgc-hotkey-cute-re"
const priority = 1080
export const loader = defineSettingPageData({
iconName,
icon: iconName,
name: "titles.shortcuts",
priority,
})

View File

@ -3,7 +3,10 @@
## New Features
- Feed owners can now reset their feeds.
- Now you can export the data from the local database.
## Improvements
- Optimized the Zen mode experience on macOS.
## Bug Fixes

View File

@ -26,7 +26,7 @@ const ymlMapsMap = {
win32: "latest.yml",
}
const keepModules = new Set(["font-list", "vscode-languagedetection"])
const keepModules = new Set(["font-list", "vscode-languagedetection", "fast-folder-size"])
const keepLanguages = new Set(["en", "en_GB", "en-US", "en_US"])
// remove folders & files not to be included in the app

View File

@ -86,7 +86,6 @@
"general.mark_as_read.render.label": "تحديد كمقروءة عند العرض",
"general.mark_as_read.scroll.description": "تحديد الإدخالات تلقائيًا كمقروءة عند التمرير خارج العرض.",
"general.mark_as_read.scroll.label": "تحديد كمقروءة عند التمرير",
"general.privacy_data": "الخصوصية والبيانات",
"general.rebuild_database.button": "إعادة بناء",
"general.rebuild_database.description": "إذا كنت تواجه مشاكل في العرض، قد تحل إعادة بناء قاعدة البيانات هذه المشاكل.",
"general.rebuild_database.label": "إعادة بناء قاعدة البيانات",

View File

@ -92,7 +92,6 @@
"general.mark_as_read.render.label": "تحديد كمقروءة عند العرض",
"general.mark_as_read.scroll.description": "تحديد الإدخالات تلقائيًا كمقروءة عند التمرير خارج العرض.",
"general.mark_as_read.scroll.label": "تحديد كمقروءة عند التمرير",
"general.privacy_data": "الخصوصية والبيانات",
"general.rebuild_database.button": "إعادة بناء",
"general.rebuild_database.description": "إذا كنت تواجه مشاكل في العرض، قد تحل إعادة بناء قاعدة البيانات هذه المشاكل.",
"general.rebuild_database.label": "إعادة بناء قاعدة البيانات",

View File

@ -92,7 +92,6 @@
"general.mark_as_read.render.label": "تحديد كمقروءة عند العرض",
"general.mark_as_read.scroll.description": "تحديد المشاركات كمقروءة تلقائيًا عند التمرير خارج العرض.",
"general.mark_as_read.scroll.label": "تحديد كمقروءة عند التمرير",
"general.privacy_data": "الخصوصية والبيانات",
"general.rebuild_database.button": "إعادة بناء",
"general.rebuild_database.description": "إذا كنت تواجه مشاكل في العرض، قد تحل إعادة بناء قاعدة البيانات هذه المشاكل.",
"general.rebuild_database.label": "إعادة بناء قاعدة البيانات",

View File

@ -86,7 +86,6 @@
"general.mark_as_read.render.label": "تحديد كمقروء عند العرض",
"general.mark_as_read.scroll.description": "تحديد المدخلات كمقروءة تلقائيًا فاش كتخرج من العرض.",
"general.mark_as_read.scroll.label": "تحديد كمقروء عند التمرير",
"general.privacy_data": "الخصوصية والبيانات",
"general.rebuild_database.button": "إعادة بناء",
"general.rebuild_database.description": "إلى كنتي كتواجه مشاكل فالعرض، إعادة بناء قاعدة البيانات غادي يحلها.",
"general.rebuild_database.label": "إعادة بناء قاعدة البيانات",

View File

@ -86,7 +86,6 @@
"general.mark_as_read.render.label": "وضع علامة كمقروء عند العرض",
"general.mark_as_read.scroll.description": "وضع علامة على الإدخالات كمقروءة تلقائيًا عند التمرير خارج العرض.",
"general.mark_as_read.scroll.label": "وضع علامة كمقروء عند التمرير",
"general.privacy_data": "الخصوصية والبيانات",
"general.rebuild_database.button": "إعادة بناء",
"general.rebuild_database.description": "إذا كنت تواجه مشكلات في العرض، فقد تحل إعادة بناء قاعدة البيانات هذه المشكلة.",
"general.rebuild_database.label": "إعادة بناء قاعدة البيانات",

View File

@ -92,7 +92,6 @@
"general.mark_as_read.render.label": "تحديد كمقروء عند العرض",
"general.mark_as_read.scroll.description": "تحديد العناصر تلقائيًا كمقروءة عند التمرير خارج العرض.",
"general.mark_as_read.scroll.label": "تحديد كمقروء عند التمرير",
"general.privacy_data": "الخصوصية والبيانات",
"general.rebuild_database.button": "إعادة بناء",
"general.rebuild_database.description": "إذا كنت تواجه مشكلات في العرض، فقد يحل إعادة بناء قاعدة البيانات هذه المشكلات.",
"general.rebuild_database.label": "إعادة بناء قاعدة البيانات",

View File

@ -96,7 +96,6 @@
"general.mark_as_read.scroll.description": "Einträge automatisch als gelesen markieren, wenn sie aus dem Ansichtsbereich gescrollt werden.",
"general.mark_as_read.scroll.label": "Als gelesen markieren, wenn gescrollt wird",
"general.network": "Netzwerk",
"general.privacy_data": "Datenschutz & Daten",
"general.proxy.description": "Proxy für Netzwerkverkehrsrouting einrichten, z.B. socks://proxy.beispiel.de:1080",
"general.proxy.label": "Proxy",
"general.rebuild_database.button": "Neu aufbauen",

View File

@ -95,6 +95,10 @@
"appearance.zen_mode.description": "Zen mode is an undisturbed reading mode that allows you to focus on the content without any interference. Enabling Zen mode will hide the sidebar.",
"appearance.zen_mode.label": "Zen mode",
"common.give_star": "<HeartIcon />Love our product? <Link>Give us a star on GitHub!</Link>",
"data_control.app_cache_limit.description": "The maximum size of the app cache. Once the cache reaches this size, the oldest items will be deleted to free up space.",
"data_control.app_cache_limit.label": "App Cache Limit",
"data_control.clean_cache.button": "Clean Cache",
"data_control.clean_cache.description": "Clean the app cache to free up space.",
"feeds.claimTips": "To claim your feeds and receive tips, right-click on the feed in your subscription list and select Claim.",
"feeds.noFeeds": "No claimed feeds",
"feeds.tableHeaders.entryCount": "Entries",
@ -102,6 +106,8 @@
"feeds.tableHeaders.subscriptionCount": "Subs",
"feeds.tableHeaders.tipAmount": "Tips",
"general.app": "App",
"general.cache": "Cache",
"general.data": "Data",
"general.data_persist.description": "Persist data locally to enable offline access and local search.",
"general.data_persist.label": "Persist data for offline usage",
"general.export.button": "Export",
@ -121,7 +127,7 @@
"general.mark_as_read.scroll.description": "Automatically mark entries as read when scrolled out of the view.",
"general.mark_as_read.scroll.label": "Mark as read when scrolling",
"general.network": "Network",
"general.privacy_data": "Privacy & Data",
"general.privacy": "Privacy",
"general.proxy.description": "Set proxy for network traffic routing, e.g., socks://proxy.example.com:1080",
"general.proxy.label": "Proxy",
"general.rebuild_database.button": "Rebuild",
@ -243,6 +249,7 @@
"titles.about": "About",
"titles.actions": "Actions",
"titles.appearance": "Appearance",
"titles.data_control": "Data Control",
"titles.feeds": "Feeds",
"titles.general": "General",
"titles.integration": "Integration",

View File

@ -86,7 +86,6 @@
"general.mark_as_read.render.label": "Marcar como leído cuando está en vista",
"general.mark_as_read.scroll.description": "Marcar automáticamente las entradas como leídas al desplazarse fuera de la vista.",
"general.mark_as_read.scroll.label": "Marcar como leído al desplazarse",
"general.privacy_data": "Privacidad y Datos",
"general.rebuild_database.button": "Reconstruir",
"general.rebuild_database.description": "Si experimentas problemas de renderizado, reconstruir la base de datos puede solucionarlos.",
"general.rebuild_database.label": "Reconstruir Base de Datos",

View File

@ -86,7 +86,6 @@
"general.mark_as_read.render.label": "Merkitse luetuksi, kun näkyvissä",
"general.mark_as_read.scroll.description": "Merkitse merkinnät automaattisesti luetuiksi, kun ne vieritetään pois näkymästä.",
"general.mark_as_read.scroll.label": "Merkitse luetuksi vierittäessä",
"general.privacy_data": "Yksityisyys ja data",
"general.rebuild_database.button": "Rakenna uudelleen",
"general.rebuild_database.description": "Jos kohtaat renderöintiongelmia, tietokannan uudelleenrakentaminen saattaa ratkaista ne.",
"general.rebuild_database.label": "Rakenna tietokanta uudelleen",

View File

@ -86,7 +86,6 @@
"general.mark_as_read.render.label": "Marquer comme lu lors de l'affichage",
"general.mark_as_read.scroll.description": "Marquer automatiquement les entrées comme lues lorsqu'elles sortent de la vue.",
"general.mark_as_read.scroll.label": "Marquer comme lu lors du défilement",
"general.privacy_data": "Confidentialité & Données",
"general.rebuild_database.button": "Reconstruire",
"general.rebuild_database.description": "Si vous rencontrez des problèmes d'affichage, la reconstruction de la base de données peut les résoudre.",
"general.rebuild_database.label": "Reconstruire la base de données",

View File

@ -86,7 +86,6 @@
"general.mark_as_read.render.label": "Segna come letto quando è in visualizzazione",
"general.mark_as_read.scroll.description": "Segna automaticamente le voci come lette quando scorri fuori dalla visualizzazione.",
"general.mark_as_read.scroll.label": "Segna come letto durante lo scorrimento",
"general.privacy_data": "Privacy e Dati",
"general.rebuild_database.button": "Ricostruisci",
"general.rebuild_database.description": "Se riscontri problemi di visualizzazione, la ricostruzione del database potrebbe risolverli.",
"general.rebuild_database.label": "Ricostruisci Database",

View File

@ -118,7 +118,6 @@
"general.mark_as_read.scroll.description": "表示からスクロールアウトしたときにエントリを自動的に既読にします。",
"general.mark_as_read.scroll.label": "スクロール時に既読にする",
"general.network": "ネットワーク",
"general.privacy_data": "プライバシーとデータ",
"general.proxy.description": "ネットワークリクエストを代理します。例: socks://proxy.example.com:1080",
"general.proxy.label": "プロキシ",
"general.rebuild_database.button": "再構築",

View File

@ -96,7 +96,6 @@
"general.mark_as_read.scroll.description": "스크롤하여 보기에서 벗어날 때 자동으로 항목을 읽음으로 표시합니다.",
"general.mark_as_read.scroll.label": "스크롤 시 읽음으로 표시",
"general.network": "네트워크",
"general.privacy_data": "개인정보 및 데이터",
"general.proxy.description": "네트워크 트래픽 라우팅을 위한 프록시 설정, 예: socks://proxy.example.com:1080",
"general.proxy.label": "프록시",
"general.rebuild_database.button": "재구축",

View File

@ -86,7 +86,6 @@
"general.mark_as_read.render.label": "Marcar como lido ao visualizar",
"general.mark_as_read.scroll.description": "Marcar automaticamente as entradas como lidas quando saem do campo de visão.",
"general.mark_as_read.scroll.label": "Marcar como lido ao rolar a página",
"general.privacy_data": "Privacidade & Dados",
"general.rebuild_database.button": "Reconstruir",
"general.rebuild_database.description": "Se estiver a ter problemas de renderização, reconstruir a base de dados pode resolver.",
"general.rebuild_database.label": "Reconstruir Base de Dados",

View File

@ -86,7 +86,6 @@
"general.mark_as_read.render.label": "Отметить как прочитанное при отображении",
"general.mark_as_read.scroll.description": "Автоматически отмечать записи как прочитанные при прокрутке.",
"general.mark_as_read.scroll.label": "Отметить как прочитанное при прокрутке",
"general.privacy_data": "Конфиденциальность и данные",
"general.rebuild_database.button": "Перестроить",
"general.rebuild_database.description": "Если у вас возникают проблемы с отображением, перестроение базы данных может их решить.",
"general.rebuild_database.label": "Перестроить базу данных",

View File

@ -96,7 +96,6 @@
"general.mark_as_read.scroll.description": "Görünümden kaydırıldığında girdileri otomatik olarak okundu olarak işaretle.",
"general.mark_as_read.scroll.label": "Kaydırıldığında okundu olarak işaretle",
"general.network": "Ağ",
"general.privacy_data": "Gizlilik ve Veri",
"general.proxy.description": "Ağ trafiği yönlendirmesi için proxy ayarla, örn., socks://proxy.example.com:1080",
"general.proxy.label": "Proxy",
"general.rebuild_database.button": "Yeniden Oluştur",

View File

@ -117,7 +117,6 @@
"general.mark_as_read.scroll.description": "当条目滚动出窗口时自动标记为已读",
"general.mark_as_read.scroll.label": "滚动时标记为已读",
"general.network": "网络",
"general.privacy_data": "隐私与数据",
"general.proxy.description": "代理网络请求示例socks://proxy.example.com:1080",
"general.proxy.label": "代理",
"general.rebuild_database.button": "重建",

View File

@ -106,7 +106,6 @@
"general.mark_as_read.scroll.description": "當滾動出視圖時,自動標記條目為已讀。",
"general.mark_as_read.scroll.label": "滾動時標記為已讀",
"general.network": "網絡",
"general.privacy_data": "隱私與數據",
"general.proxy.description": "代理網絡流量例如socks://proxy.example.com:1080",
"general.proxy.label": "代理",
"general.rebuild_database.button": "重建",

View File

@ -106,7 +106,6 @@
"general.mark_as_read.scroll.description": "當條目捲動離開視圖時自動標記為已讀。",
"general.mark_as_read.scroll.label": "捲動時標記為已讀",
"general.network": "網路",
"general.privacy_data": "隱私與資料",
"general.proxy.description": "設定代理伺服器來處理網路流量例如socks://proxy.example.com:1080",
"general.proxy.label": "代理伺服器",
"general.rebuild_database.button": "重置",

View File

@ -131,7 +131,8 @@
"prettier --ignore-unknown --write"
],
"locales/**/*.json": [
"npm run dedupe:locales"
"npm run dedupe:locales",
"git add locales"
]
},
"bump": {

View File

@ -0,0 +1,12 @@
import type { SVGProps } from "react"
export function MaterialSymbolsDatabaseOutline(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12 21q-3.775 0-6.387-1.162T3 17V7q0-1.65 2.638-2.825T12 3t6.363 1.175T21 7v10q0 1.675-2.613 2.838T12 21m0-11.975q2.225 0 4.475-.638T19 7.025q-.275-.725-2.512-1.375T12 5q-2.275 0-4.462.638T5 7.025q.35.75 2.538 1.375T12 9.025M12 14q1.05 0 2.025-.1t1.863-.288t1.675-.462T19 12.525v-3q-.65.35-1.437.625t-1.675.463t-1.863.287T12 11t-2.05-.1t-1.888-.288T6.4 10.15T5 9.525v3q.625.35 1.4.625t1.663.463t1.887.287T12 14m0 5q1.15 0 2.338-.175t2.187-.462t1.675-.65t.8-.738v-2.45q-.65.35-1.437.625t-1.675.463t-1.863.287T12 16t-2.05-.1t-1.888-.288T6.4 15.15T5 14.525V17q.125.375.788.725t1.662.638t2.2.462T12 19"
/>
</svg>
)
}

View File

@ -0,0 +1,12 @@
import type { SVGProps } from "react"
export function CarbonInfinitySymbol(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32" {...props}>
<path
fill="currentColor"
d="M23 23c-5.656 0-7.858-6.41-7.949-6.684C15.034 16.265 13.208 11 9 11c-2.757 0-5 2.243-5 5s2.243 5 5 5c1.588 0 3.013-.732 4.237-2.176l1.526 1.293C13.164 22.003 11.172 23 9 23c-3.86 0-7-3.14-7-7s3.14-7 7-7c5.656 0 7.858 6.41 7.949 6.684C16.966 15.735 18.792 21 23 21c2.757 0 5-2.243 5-5s-2.243-5-5-5c-1.588 0-3.013.732-4.237 2.176l-1.526-1.293C18.836 9.997 20.828 9 23 9c3.86 0 7 3.14 7 7s-3.14 7-7 7"
/>
</svg>
)
}

View File

@ -0,0 +1,20 @@
import { cn } from "@follow/utils/utils"
import * as SliderPrimitive from "@radix-ui/react-slider"
import * as React from "react"
export const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-accent/20">
<SliderPrimitive.Range className="absolute h-full bg-accent" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block size-4 rounded-full border border-accent/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName

View File

@ -272,6 +272,9 @@ importers:
electron-updater:
specifier: ^6.3.9
version: 6.3.9
fast-folder-size:
specifier: 2.3.0
version: 2.3.0
font-list:
specifier: 1.5.1
version: 1.5.1
@ -3343,8 +3346,8 @@ packages:
'@radix-ui/react-avatar@1.1.1':
resolution: {integrity: sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==}
peerDependencies:
'@types/react': npm:types-react@19.0.0-rc.1
'@types/react-dom': npm:types-react-dom@19.0.0-rc.1
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -4859,6 +4862,9 @@ packages:
birecord@0.1.1:
resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==}
bl@1.2.3:
resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@ -4936,6 +4942,12 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer-alloc-unsafe@1.1.0:
resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
buffer-alloc@1.2.0:
resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==}
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
@ -4946,6 +4958,9 @@ packages:
resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==}
engines: {node: '>=0.4'}
buffer-fill@1.0.0:
resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@ -5542,6 +5557,26 @@ packages:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
decompress-tar@4.1.1:
resolution: {integrity: sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==}
engines: {node: '>=4'}
decompress-tarbz2@4.1.1:
resolution: {integrity: sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==}
engines: {node: '>=4'}
decompress-targz@4.1.1:
resolution: {integrity: sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==}
engines: {node: '>=4'}
decompress-unzip@4.0.1:
resolution: {integrity: sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==}
engines: {node: '>=4'}
decompress@4.2.1:
resolution: {integrity: sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==}
engines: {node: '>=4'}
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
@ -6233,6 +6268,10 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-folder-size@2.3.0:
resolution: {integrity: sha512-W3HuQYdZ1GaOAdIqKR/guqxvlHW0f8ZFknYfnsTBuL+9QokVARUBNt/kFE8hkyZd/yQ/mYnKmrtWmFyTLRfX3A==}
hasBin: true
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
@ -6293,6 +6332,18 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
file-type@3.9.0:
resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==}
engines: {node: '>=0.10.0'}
file-type@5.2.0:
resolution: {integrity: sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==}
engines: {node: '>=4'}
file-type@6.2.0:
resolution: {integrity: sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==}
engines: {node: '>=4'}
filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
@ -6499,6 +6550,10 @@ packages:
resolution: {integrity: sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==}
engines: {node: '>= 4.0'}
get-stream@2.3.1:
resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==}
engines: {node: '>=0.10.0'}
get-stream@4.1.0:
resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==}
engines: {node: '>=6'}
@ -6974,6 +7029,9 @@ packages:
is-my-json-valid@2.20.6:
resolution: {integrity: sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==}
is-natural-number@4.0.1:
resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
@ -7451,6 +7509,10 @@ packages:
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
engines: {node: '>=12'}
make-dir@1.3.0:
resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==}
engines: {node: '>=4'}
make-fetch-happen@10.2.1:
resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@ -8190,6 +8252,18 @@ packages:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pify@3.0.0:
resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
engines: {node: '>=4'}
pinkie-promise@2.0.1:
resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==}
engines: {node: '>=0.10.0'}
pinkie@2.0.4:
resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
engines: {node: '>=0.10.0'}
pino-abstract-transport@2.0.0:
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
@ -9059,6 +9133,10 @@ packages:
secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
seek-bzip@1.0.6:
resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==}
hasBin: true
selecto@1.26.3:
resolution: {integrity: sha512-gZHgqMy5uyB6/2YDjv3Qqaf7bd2hTDOpPdxXlrez4R3/L0GiEWDCFaUfrflomgqdb3SxHF2IXY0Jw0EamZi7cw==}
@ -9315,6 +9393,9 @@ packages:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
strip-dirs@2.1.0:
resolution: {integrity: sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==}
strip-eof@1.0.0:
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
engines: {node: '>=0.10.0'}
@ -9409,6 +9490,10 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
tar-stream@1.6.2:
resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==}
engines: {node: '>= 0.8.0'}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
@ -9442,6 +9527,9 @@ packages:
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
@ -9488,6 +9576,9 @@ packages:
resolution: {integrity: sha512-DbplOfQFkqG5IHcDyyrs/lkvSr3mPUVsFf/RbDppOshs22yTPnSJWEe6FkYd1txAwU/zcnR905ar2fi4kwF29w==}
engines: {node: '>=0.12'}
to-buffer@1.1.1:
resolution: {integrity: sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==}
to-data-view@1.1.0:
resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==}
@ -9672,6 +9763,9 @@ packages:
uhyphen@0.2.0:
resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==}
unbzip2-stream@1.4.3:
resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==}
unconfig@0.5.5:
resolution: {integrity: sha512-VQZ5PT9HDX+qag0XdgQi8tJepPhXiR/yVOkn707gJDKo31lGjRilPREiQJ9Z6zd/Ugpv6ZvO5VxVIcatldYcNQ==}
@ -14879,6 +14973,11 @@ snapshots:
birecord@0.1.1: {}
bl@1.2.3:
dependencies:
readable-stream: 2.3.8
safe-buffer: 5.2.1
bl@4.1.0:
dependencies:
buffer: 5.7.1
@ -14983,12 +15082,21 @@ snapshots:
node-releases: 2.0.18
update-browserslist-db: 1.1.1(browserslist@4.24.0)
buffer-alloc-unsafe@1.1.0: {}
buffer-alloc@1.2.0:
dependencies:
buffer-alloc-unsafe: 1.1.0
buffer-fill: 1.0.0
buffer-crc32@0.2.13: {}
buffer-equal-constant-time@1.0.1: {}
buffer-equal@1.0.1: {}
buffer-fill@1.0.0: {}
buffer-from@1.1.2: {}
buffer-xor@1.0.3: {}
@ -15705,6 +15813,44 @@ snapshots:
dependencies:
mimic-response: 3.1.0
decompress-tar@4.1.1:
dependencies:
file-type: 5.2.0
is-stream: 1.1.0
tar-stream: 1.6.2
decompress-tarbz2@4.1.1:
dependencies:
decompress-tar: 4.1.1
file-type: 6.2.0
is-stream: 1.1.0
seek-bzip: 1.0.6
unbzip2-stream: 1.4.3
decompress-targz@4.1.1:
dependencies:
decompress-tar: 4.1.1
file-type: 5.2.0
is-stream: 1.1.0
decompress-unzip@4.0.1:
dependencies:
file-type: 3.9.0
get-stream: 2.3.1
pify: 2.3.0
yauzl: 2.10.0
decompress@4.2.1:
dependencies:
decompress-tar: 4.1.1
decompress-tarbz2: 4.1.1
decompress-targz: 4.1.1
decompress-unzip: 4.0.1
graceful-fs: 4.2.11
make-dir: 1.3.0
pify: 2.3.0
strip-dirs: 2.1.0
deep-eql@5.0.2: {}
deep-is@0.1.4: {}
@ -16610,6 +16756,13 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-folder-size@2.3.0:
dependencies:
decompress: 4.2.1
https-proxy-agent: 7.0.5
transitivePeerDependencies:
- supports-color
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -16686,6 +16839,12 @@ snapshots:
dependencies:
flat-cache: 4.0.1
file-type@3.9.0: {}
file-type@5.2.0: {}
file-type@6.2.0: {}
filelist@1.0.4:
dependencies:
minimatch: 5.1.6
@ -16944,6 +17103,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
get-stream@2.3.1:
dependencies:
object-assign: 4.1.1
pinkie-promise: 2.0.1
get-stream@4.1.0:
dependencies:
pump: 3.0.2
@ -17595,6 +17759,8 @@ snapshots:
xtend: 4.0.2
optional: true
is-natural-number@4.0.1: {}
is-number@7.0.0: {}
is-obj@2.0.0: {}
@ -18033,6 +18199,10 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
make-dir@1.3.0:
dependencies:
pify: 3.0.0
make-fetch-happen@10.2.1:
dependencies:
agentkeepalive: 4.5.0
@ -19039,6 +19209,14 @@ snapshots:
pify@2.3.0: {}
pify@3.0.0: {}
pinkie-promise@2.0.1:
dependencies:
pinkie: 2.0.4
pinkie@2.0.4: {}
pino-abstract-transport@2.0.0:
dependencies:
split2: 4.2.0
@ -19966,6 +20144,10 @@ snapshots:
secure-json-parse@2.7.0: {}
seek-bzip@1.0.6:
dependencies:
commander: 2.20.3
selecto@1.26.3:
dependencies:
'@daybrush/utils': 1.13.0
@ -20218,6 +20400,10 @@ snapshots:
strip-bom@3.0.0: {}
strip-dirs@2.1.0:
dependencies:
is-natural-number: 4.0.1
strip-eof@1.0.0: {}
strip-final-newline@3.0.0: {}
@ -20332,6 +20518,16 @@ snapshots:
transitivePeerDependencies:
- ts-node
tar-stream@1.6.2:
dependencies:
bl: 1.2.3
buffer-alloc: 1.2.0
end-of-stream: 1.4.4
fs-constants: 1.0.0
readable-stream: 2.3.8
to-buffer: 1.1.1
xtend: 4.0.2
tar-stream@2.2.0:
dependencies:
bl: 4.1.0
@ -20381,6 +20577,8 @@ snapshots:
dependencies:
real-require: 0.2.0
through@2.3.8: {}
tiny-inflate@1.0.3: {}
tiny-typed-emitter@2.1.0: {}
@ -20417,6 +20615,8 @@ snapshots:
unorm: 1.6.0
optional: true
to-buffer@1.1.1: {}
to-data-view@1.1.0:
optional: true
@ -20572,6 +20772,11 @@ snapshots:
uhyphen@0.2.0: {}
unbzip2-stream@1.4.3:
dependencies:
buffer: 5.7.1
through: 2.3.8
unconfig@0.5.5:
dependencies:
'@antfu/utils': 0.7.10