feat: add discover feed form from rsshub (#89)
* feat: regexp path parser Signed-off-by: Innei <i@innei.in> * feat: markdown Signed-off-by: Innei <i@innei.in> * fix: shirk Signed-off-by: Innei <i@innei.in> * feat: discover form Signed-off-by: Innei <i@innei.in> * fix: modal overlay zindex Signed-off-by: Innei <i@innei.in> * feat: fill path Signed-off-by: Innei <i@innei.in> * feat: modal Signed-off-by: Innei <i@innei.in> * fix: card style Signed-off-by: Innei <i@innei.in> * fix: parser Signed-off-by: Innei <i@innei.in> * feat: add maintainers Signed-off-by: Innei <i@innei.in> * fix: select zindex Signed-off-by: Innei <i@innei.in> * fix: dismiss all Signed-off-by: Innei <i@innei.in> * feat: optimize a11y Signed-off-by: Innei <i@innei.in> * fix: dark mode style Signed-off-by: Innei <i@innei.in> --------- Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
43e7d12a54
commit
57b643fced
|
|
@ -36,4 +36,5 @@ jobs:
|
|||
run: |
|
||||
pnpm run typecheck
|
||||
npm run lint
|
||||
pnpm run test
|
||||
- name: Run test
|
||||
run: pnpm test
|
||||
|
|
|
|||
|
|
@ -76,7 +76,11 @@
|
|||
"lodash-es": "4.17.21",
|
||||
"lowdb": "7.0.1",
|
||||
"lucide-react": "0.396.0",
|
||||
"markdown-it": "14.1.0",
|
||||
"markdown-it-container": "4.0.0",
|
||||
"ofetch": "1.3.4",
|
||||
"path-to-regexp": "7.0.0",
|
||||
"react-error-boundary": "4.0.13",
|
||||
"react-helmet-async": "2.0.5",
|
||||
"react-hook-form": "7.52.0",
|
||||
"react-hotkeys-hook": "4.5.0",
|
||||
|
|
@ -108,6 +112,8 @@
|
|||
"@tailwindcss/container-queries": "0.1.1",
|
||||
"@tailwindcss/typography": "0.5.13",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/markdown-it": "14.1.1",
|
||||
"@types/markdown-it-container": "2.0.10",
|
||||
"@types/node": "^20.14.7",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
|
|
|
|||
100
pnpm-lock.yaml
100
pnpm-lock.yaml
|
|
@ -169,9 +169,21 @@ importers:
|
|||
lucide-react:
|
||||
specifier: 0.396.0
|
||||
version: 0.396.0(react@18.3.1)
|
||||
markdown-it:
|
||||
specifier: 14.1.0
|
||||
version: 14.1.0
|
||||
markdown-it-container:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
ofetch:
|
||||
specifier: 1.3.4
|
||||
version: 1.3.4
|
||||
path-to-regexp:
|
||||
specifier: 7.0.0
|
||||
version: 7.0.0
|
||||
react-error-boundary:
|
||||
specifier: 4.0.13
|
||||
version: 4.0.13(react@18.3.1)
|
||||
react-helmet-async:
|
||||
specifier: 2.0.5
|
||||
version: 2.0.5(react@18.3.1)
|
||||
|
|
@ -260,6 +272,12 @@ importers:
|
|||
'@types/lodash-es':
|
||||
specifier: 4.17.12
|
||||
version: 4.17.12
|
||||
'@types/markdown-it':
|
||||
specifier: 14.1.1
|
||||
version: 14.1.1
|
||||
'@types/markdown-it-container':
|
||||
specifier: 2.0.10
|
||||
version: 2.0.10
|
||||
'@types/node':
|
||||
specifier: ^20.14.7
|
||||
version: 20.14.7
|
||||
|
|
@ -2179,15 +2197,27 @@ packages:
|
|||
'@types/keyv@3.1.4':
|
||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||
|
||||
'@types/linkify-it@5.0.0':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
'@types/lodash-es@4.17.12':
|
||||
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
|
||||
|
||||
'@types/lodash@4.17.4':
|
||||
resolution: {integrity: sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==}
|
||||
|
||||
'@types/markdown-it-container@2.0.10':
|
||||
resolution: {integrity: sha512-zv+YxrlSYRq51e9kzm3orv4OvF4U79Ll1OyplNXr00o4ZC/8PukJk/jEWH7CnsMtrSWZlyv0czhz42jm9J4uLw==}
|
||||
|
||||
'@types/markdown-it@14.1.1':
|
||||
resolution: {integrity: sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==}
|
||||
|
||||
'@types/mdast@4.0.3':
|
||||
resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==}
|
||||
|
||||
'@types/mdurl@2.0.0':
|
||||
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||
|
||||
'@types/ms@0.7.34':
|
||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
||||
|
||||
|
|
@ -4145,6 +4175,9 @@ packages:
|
|||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||
|
||||
lint-staged@15.2.7:
|
||||
resolution: {integrity: sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
|
@ -4259,6 +4292,13 @@ packages:
|
|||
resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
|
||||
markdown-it-container@4.0.0:
|
||||
resolution: {integrity: sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
hasBin: true
|
||||
|
||||
matcher@3.0.0:
|
||||
resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -4293,6 +4333,9 @@ packages:
|
|||
mdn-data@2.0.30:
|
||||
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
||||
|
||||
mdurl@2.0.0:
|
||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||
|
||||
merge-stream@2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
|
||||
|
|
@ -4695,6 +4738,10 @@ packages:
|
|||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-to-regexp@7.0.0:
|
||||
resolution: {integrity: sha512-58Y94bQqF3zBIASFNiufRPH1NfgZth1qwZ35radL87sg8pgbVqr6uikAhqZtFD+w65MGH6SWnY/ly3GbrM4fbg==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
path-type@4.0.0:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -4834,6 +4881,10 @@ packages:
|
|||
pump@3.0.0:
|
||||
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
|
||||
|
||||
punycode.js@2.3.1:
|
||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -4850,6 +4901,11 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
|
||||
react-error-boundary@4.0.13:
|
||||
resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.13.1'
|
||||
|
||||
react-fast-compare@3.2.2:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
|
||||
|
|
@ -5503,6 +5559,9 @@ packages:
|
|||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
uc.micro@2.1.0:
|
||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||
|
||||
ufo@1.5.3:
|
||||
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
|
||||
|
||||
|
|
@ -7797,16 +7856,29 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 20.14.7
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/lodash-es@4.17.12':
|
||||
dependencies:
|
||||
'@types/lodash': 4.17.4
|
||||
|
||||
'@types/lodash@4.17.4': {}
|
||||
|
||||
'@types/markdown-it-container@2.0.10':
|
||||
dependencies:
|
||||
'@types/markdown-it': 14.1.1
|
||||
|
||||
'@types/markdown-it@14.1.1':
|
||||
dependencies:
|
||||
'@types/linkify-it': 5.0.0
|
||||
'@types/mdurl': 2.0.0
|
||||
|
||||
'@types/mdast@4.0.3':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.2
|
||||
|
||||
'@types/mdurl@2.0.0': {}
|
||||
|
||||
'@types/ms@0.7.34': {}
|
||||
|
||||
'@types/node@20.14.7':
|
||||
|
|
@ -10254,6 +10326,10 @@ snapshots:
|
|||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
|
||||
lint-staged@15.2.7:
|
||||
dependencies:
|
||||
chalk: 5.3.0
|
||||
|
|
@ -10390,6 +10466,17 @@ snapshots:
|
|||
- bluebird
|
||||
- supports-color
|
||||
|
||||
markdown-it-container@4.0.0: {}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
entities: 4.5.0
|
||||
linkify-it: 5.0.0
|
||||
mdurl: 2.0.0
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 2.1.0
|
||||
|
||||
matcher@3.0.0:
|
||||
dependencies:
|
||||
escape-string-regexp: 4.0.0
|
||||
|
|
@ -10488,6 +10575,8 @@ snapshots:
|
|||
|
||||
mdn-data@2.0.30: {}
|
||||
|
||||
mdurl@2.0.0: {}
|
||||
|
||||
merge-stream@2.0.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
|
@ -10964,6 +11053,8 @@ snapshots:
|
|||
lru-cache: 10.2.2
|
||||
minipass: 7.1.2
|
||||
|
||||
path-to-regexp@7.0.0: {}
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
pathe@1.1.2: {}
|
||||
|
|
@ -11077,6 +11168,8 @@ snapshots:
|
|||
end-of-stream: 1.4.4
|
||||
once: 1.4.0
|
||||
|
||||
punycode.js@2.3.1: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
|
@ -11089,6 +11182,11 @@ snapshots:
|
|||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
react-error-boundary@4.0.13(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.4
|
||||
react: 18.3.1
|
||||
|
||||
react-fast-compare@3.2.2: {}
|
||||
|
||||
react-helmet-async@2.0.5(react@18.3.1):
|
||||
|
|
@ -11786,6 +11884,8 @@ snapshots:
|
|||
|
||||
typescript@5.5.2: {}
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
ufo@1.5.3: {}
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const registerAppMenu = () => {
|
|||
label: "follow https://rsshub.app/twitter/user/DIYgod",
|
||||
click: () => {
|
||||
createWindow({
|
||||
extraPath: `/add?url=${encodeURIComponent(
|
||||
extraPath: `#add?url=${encodeURIComponent(
|
||||
"https://rsshub.app/twitter/user/DIYgod",
|
||||
)}`,
|
||||
width: 800,
|
||||
|
|
@ -55,7 +55,7 @@ export const registerAppMenu = () => {
|
|||
label: "follow https://diygod.me/feed",
|
||||
click: () => {
|
||||
createWindow({
|
||||
extraPath: `/add?url=${encodeURIComponent(
|
||||
extraPath: `#add?url=${encodeURIComponent(
|
||||
"https://diygod.me/feed",
|
||||
)}`,
|
||||
width: 800,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import { createAtomHooks } from "@renderer/lib/jotai"
|
||||
import { atom } from "jotai"
|
||||
|
||||
export const [
|
||||
,
|
||||
useSidebarActiveView,
|
||||
useSidebarActiveViewValue,
|
||||
useSetSidebarActiveView,
|
||||
getSidebarActiveView,
|
||||
setSidebarActiveView,
|
||||
] = createAtomHooks(atom(0))
|
||||
|
|
@ -36,6 +36,7 @@ export const CopyButton: Component<{
|
|||
}, [value])
|
||||
return (
|
||||
<MotionButtonBase
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
"center flex text-xs",
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ const FormItem = React.forwardRef<
|
|||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<FormItemContext.Provider value={React.useMemo(() => ({ id }), [id])}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable @eslint-react/dom/no-dangerously-set-innerhtml */
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import markdownit from "markdown-it"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import styles from "./index.module.css"
|
||||
import { createContainer } from "./plugins/container"
|
||||
|
||||
const md = markdownit()
|
||||
.use(...createContainer("tip", "TIP", () => md))
|
||||
.use(...createContainer("info", "INFO", () => md))
|
||||
.use(...createContainer("warning", "WARNING", () => md))
|
||||
.use(...createContainer("danger", "DANGER", () => md))
|
||||
.use(...createContainer("details", "Details", () => md))
|
||||
|
||||
export const Markdown: Component<{
|
||||
children: string
|
||||
}> = (props) => (
|
||||
<div
|
||||
className={cn(styles.markdown, props.className, "prose dark:prose-invert prose-th:text-left")}
|
||||
dangerouslySetInnerHTML={useMemo(
|
||||
() => ({
|
||||
__html: md.render(props.children as string),
|
||||
}),
|
||||
[props.children],
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
.markdown {
|
||||
--vp-custom-block-font-size: 16px;
|
||||
--vp-custom-block-code-font-size: 14px;
|
||||
--vp-custom-block-info-border: #0077cc;
|
||||
--vp-custom-block-info-text: #0077cc;
|
||||
--vp-custom-block-info-bg: #f0f9ff;
|
||||
--vp-custom-block-info-code-bg: #f0f9ff;
|
||||
--vp-custom-block-note-border: #f6c244;
|
||||
--vp-custom-block-note-text: #f6c244;
|
||||
--vp-custom-block-note-bg: #fff8e6;
|
||||
--vp-custom-block-note-code-bg: #fff8e6;
|
||||
--vp-custom-block-tip-border: #0077cc;
|
||||
--vp-custom-block-tip-text: #0077cc;
|
||||
--vp-custom-block-tip-bg: #f0f9ff;
|
||||
--vp-custom-block-tip-code-bg: #f0f9ff;
|
||||
--vp-custom-block-important-border: #ff4d4f;
|
||||
--vp-custom-block-important-text: #ff4d4f;
|
||||
--vp-custom-block-important-bg: #fff1f0;
|
||||
--vp-custom-block-important-code-bg: #fff1f0;
|
||||
--vp-custom-block-warning-border: #ffaa00;
|
||||
--vp-custom-block-warning-text: #ffaa00;
|
||||
--vp-custom-block-warning-bg: #fffbe6;
|
||||
--vp-custom-block-warning-code-bg: #fffbe6;
|
||||
--vp-custom-block-danger-border: #ff4d4f;
|
||||
--vp-custom-block-danger-text: #ff4d4f;
|
||||
--vp-custom-block-danger-bg: #fff1f0;
|
||||
--vp-custom-block-danger-code-bg: #fff1f0;
|
||||
--vp-custom-block-caution-border: #ffaa00;
|
||||
--vp-custom-block-caution-text: #ffaa00;
|
||||
--vp-custom-block-caution-bg: #fffbe6;
|
||||
--vp-custom-block-caution-code-bg: #fffbe6;
|
||||
--vp-custom-block-details-border: #0077cc;
|
||||
--vp-custom-block-details-text: #0077cc;
|
||||
--vp-custom-block-details-bg: #f0f9ff;
|
||||
--vp-custom-block-details-code-bg: #f0f9ff;
|
||||
|
||||
--vp-c-text-2: #333;
|
||||
--vp-c-brand-1: #0077cc;
|
||||
--vp-c-brand-2: #005faa;
|
||||
--vp-c-tip-1: #0077cc;
|
||||
--vp-c-tip-2: #005faa;
|
||||
--vp-c-important-1: #ff4d4f;
|
||||
--vp-c-important-2: #d32f2f;
|
||||
--vp-c-warning-1: #ffaa00;
|
||||
--vp-c-warning-2: #cc8400;
|
||||
--vp-c-danger-1: #ff4d4f;
|
||||
--vp-c-danger-2: #d32f2f;
|
||||
--vp-c-caution-1: #ffaa00;
|
||||
--vp-c-caution-2: #cc8400;
|
||||
|
||||
:global {
|
||||
.custom-block {
|
||||
@apply text-sm;
|
||||
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 16px 16px 8px;
|
||||
line-height: 24px;
|
||||
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.custom-block.info {
|
||||
border-color: var(--vp-custom-block-info-border);
|
||||
color: var(--vp-custom-block-info-text);
|
||||
background-color: var(--vp-custom-block-info-bg);
|
||||
}
|
||||
|
||||
.custom-block.info a,
|
||||
.custom-block.info code {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.custom-block.info a:hover,
|
||||
.custom-block.info a:hover > code {
|
||||
color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.custom-block.info code {
|
||||
background-color: var(--vp-custom-block-info-code-bg);
|
||||
}
|
||||
|
||||
.custom-block.note {
|
||||
border-color: var(--vp-custom-block-note-border);
|
||||
color: var(--vp-custom-block-note-text);
|
||||
background-color: var(--vp-custom-block-note-bg);
|
||||
}
|
||||
|
||||
.custom-block.note a,
|
||||
.custom-block.note code {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.custom-block.note a:hover,
|
||||
.custom-block.note a:hover > code {
|
||||
color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.custom-block.note code {
|
||||
background-color: var(--vp-custom-block-note-code-bg);
|
||||
}
|
||||
|
||||
.custom-block.tip {
|
||||
border-color: var(--vp-custom-block-tip-border);
|
||||
color: var(--vp-custom-block-tip-text);
|
||||
background-color: var(--vp-custom-block-tip-bg);
|
||||
}
|
||||
|
||||
.custom-block.tip a,
|
||||
.custom-block.tip code {
|
||||
color: var(--vp-c-tip-1);
|
||||
}
|
||||
|
||||
.custom-block.tip a:hover,
|
||||
.custom-block.tip a:hover > code {
|
||||
color: var(--vp-c-tip-2);
|
||||
}
|
||||
|
||||
.custom-block.tip code {
|
||||
background-color: var(--vp-custom-block-tip-code-bg);
|
||||
}
|
||||
|
||||
.custom-block.important {
|
||||
border-color: var(--vp-custom-block-important-border);
|
||||
color: var(--vp-custom-block-important-text);
|
||||
background-color: var(--vp-custom-block-important-bg);
|
||||
}
|
||||
|
||||
.custom-block.important a,
|
||||
.custom-block.important code {
|
||||
color: var(--vp-c-important-1);
|
||||
}
|
||||
|
||||
.custom-block.important a:hover,
|
||||
.custom-block.important a:hover > code {
|
||||
color: var(--vp-c-important-2);
|
||||
}
|
||||
|
||||
.custom-block.important code {
|
||||
background-color: var(--vp-custom-block-important-code-bg);
|
||||
}
|
||||
|
||||
.custom-block.warning {
|
||||
border-color: var(--vp-custom-block-warning-border);
|
||||
color: var(--vp-custom-block-warning-text);
|
||||
background-color: var(--vp-custom-block-warning-bg);
|
||||
}
|
||||
|
||||
.custom-block.warning a,
|
||||
.custom-block.warning code {
|
||||
color: var(--vp-c-warning-1);
|
||||
}
|
||||
|
||||
.custom-block.warning a:hover,
|
||||
.custom-block.warning a:hover > code {
|
||||
color: var(--vp-c-warning-2);
|
||||
}
|
||||
|
||||
.custom-block.warning code {
|
||||
background-color: var(--vp-custom-block-warning-code-bg);
|
||||
}
|
||||
|
||||
.custom-block.danger {
|
||||
border-color: var(--vp-custom-block-danger-border);
|
||||
color: var(--vp-custom-block-danger-text);
|
||||
background-color: var(--vp-custom-block-danger-bg);
|
||||
}
|
||||
|
||||
.custom-block.danger a,
|
||||
.custom-block.danger code {
|
||||
color: var(--vp-c-danger-1);
|
||||
}
|
||||
|
||||
.custom-block.danger a:hover,
|
||||
.custom-block.danger a:hover > code {
|
||||
color: var(--vp-c-danger-2);
|
||||
}
|
||||
|
||||
.custom-block.danger code {
|
||||
background-color: var(--vp-custom-block-danger-code-bg);
|
||||
}
|
||||
|
||||
.custom-block.caution {
|
||||
border-color: var(--vp-custom-block-caution-border);
|
||||
color: var(--vp-custom-block-caution-text);
|
||||
background-color: var(--vp-custom-block-caution-bg);
|
||||
}
|
||||
|
||||
.custom-block.caution a,
|
||||
.custom-block.caution code {
|
||||
color: var(--vp-c-caution-1);
|
||||
}
|
||||
|
||||
.custom-block.caution a:hover,
|
||||
.custom-block.caution a:hover > code {
|
||||
color: var(--vp-c-caution-2);
|
||||
}
|
||||
|
||||
.custom-block.caution code {
|
||||
background-color: var(--vp-custom-block-caution-code-bg);
|
||||
}
|
||||
|
||||
.custom-block.details {
|
||||
border-color: var(--vp-custom-block-details-border);
|
||||
color: var(--vp-custom-block-details-text);
|
||||
background-color: var(--vp-custom-block-details-bg);
|
||||
}
|
||||
|
||||
.custom-block.details a {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.custom-block.details a:hover,
|
||||
.custom-block.details a:hover > code {
|
||||
color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.custom-block.details code {
|
||||
background-color: var(--vp-custom-block-details-code-bg);
|
||||
}
|
||||
|
||||
.custom-block-title {
|
||||
font-weight: 600;
|
||||
@apply m-0 text-base;
|
||||
}
|
||||
|
||||
.custom-block p + p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.custom-block.details summary {
|
||||
margin: 0 0 8px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.custom-block.details summary + p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.custom-block a {
|
||||
color: inherit;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.custom-block a:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.custom-block code {
|
||||
font-size: var(--vp-custom-block-code-font-size);
|
||||
}
|
||||
|
||||
.custom-block.custom-block th,
|
||||
.custom-block.custom-block blockquote > p {
|
||||
font-size: var(--vp-custom-block-font-size);
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./Markdown"
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import type MarkdownIt from "markdown-it"
|
||||
import type { RenderRule } from "markdown-it/lib/renderer.mjs"
|
||||
import container from "markdown-it-container"
|
||||
/** @copy vuejs/vitepress/src/node/markdown/plugins/containers.ts */
|
||||
type ContainerArgs = [typeof container, string, { render: RenderRule }]
|
||||
|
||||
export function createContainer(
|
||||
klass: string,
|
||||
defaultTitle: string,
|
||||
getMd: () => MarkdownIt,
|
||||
): ContainerArgs {
|
||||
return [
|
||||
container,
|
||||
klass,
|
||||
{
|
||||
render(tokens, idx, _options, env: { references?: any }) {
|
||||
const md = getMd()
|
||||
const token = tokens[idx]
|
||||
const info = token.info.trim().slice(klass.length).trim()
|
||||
const attrs = md.renderer.renderAttrs(token)
|
||||
if (token.nesting === 1) {
|
||||
const title = md.renderInline(info || defaultTitle, {
|
||||
references: env.references,
|
||||
})
|
||||
if (klass === "details") { return `<details class="${klass} custom-block"${attrs}><summary>${title}</summary>\n` }
|
||||
return `<div class="${klass} custom-block"${attrs}><p class="custom-block-title">${title}</p>\n`
|
||||
} else { return klass === "details" ? `</details>\n` : `</div>\n` }
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { atom } from "jotai"
|
||||
|
||||
import type { ModalProps } from "./types"
|
||||
|
||||
export const modalStackAtom = atom([] as (ModalProps & { id: string })[])
|
||||
|
|
@ -17,3 +17,5 @@ export const modalMontionConfig = {
|
|||
exit: initialStyle,
|
||||
transition: microReboundPreset,
|
||||
}
|
||||
|
||||
export const MODAL_STACK_Z_INDEX = 100
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import { atom } from "jotai"
|
||||
import type { FC, RefObject } from "react"
|
||||
import { createContext, useContext } from "react"
|
||||
|
||||
import type { ModalProps } from "./types"
|
||||
|
||||
export const modalIdToPropsMap = {} as Record<string, ModalProps>
|
||||
import { createContext } from "react"
|
||||
|
||||
export type CurrentModalContentProps = ModalContentPropsInternal & {
|
||||
ref: RefObject<HTMLElement | null>
|
||||
|
|
@ -14,10 +9,7 @@ export const CurrentModalContext = createContext<CurrentModalContentProps>(
|
|||
null as any,
|
||||
)
|
||||
|
||||
export const useCurrentModal = () => useContext(CurrentModalContext)
|
||||
|
||||
export type ModalContentComponent<T> = FC<ModalContentPropsInternal & T>
|
||||
export type ModalContentPropsInternal = {
|
||||
dismiss: () => void
|
||||
}
|
||||
export const modalStackAtom = atom([] as (ModalProps & { id: string })[])
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import { AnimatePresence } from "framer-motion"
|
|||
import type { FC, ReactNode } from "react"
|
||||
import { useId, useMemo } from "react"
|
||||
|
||||
import { modalStackAtom } from "./context"
|
||||
import { modalStackAtom } from "./atom"
|
||||
import { MODAL_STACK_Z_INDEX } from "./constants"
|
||||
import { ModalInternal } from "./modal"
|
||||
import { ModalOverlay } from "./overlay"
|
||||
import type { ModalProps } from "./types"
|
||||
|
||||
export interface DeclarativeModalProps extends Omit<ModalProps, "content"> {
|
||||
|
|
@ -37,18 +39,19 @@ const DeclarativeModalImpl: FC<DeclarativeModalProps> = ({
|
|||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<ModalInternal isTop onClose={onOpenChange} index={index} item={item}>
|
||||
{children}
|
||||
</ModalInternal>
|
||||
<>
|
||||
<ModalInternal isTop onClose={onOpenChange} index={index} item={item}>
|
||||
{children}
|
||||
</ModalInternal>
|
||||
<ModalOverlay zIndex={MODAL_STACK_Z_INDEX - 1 + index} />
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
const FooterAction: Component = ({ children, className }) => (
|
||||
<div
|
||||
className={cn("mt-4 flex items-center justify-end gap-2", className)}
|
||||
>
|
||||
<div className={cn("mt-4 flex items-center justify-end gap-2", className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { jotaiStore } from "@renderer/lib/jotai"
|
||||
import { useUIStore } from "@renderer/store"
|
||||
import { useCallback, useEffect, useId, useRef } from "react"
|
||||
import { useCallback, useContext, useEffect, useId, useRef } from "react"
|
||||
import { useLocation } from "react-router-dom"
|
||||
|
||||
import { modalIdToPropsMap, modalStackAtom } from "./context"
|
||||
import { modalStackAtom } from "./atom"
|
||||
import { CurrentModalContext } from "./context"
|
||||
import type { ModalProps, ModalStackOptions } from "./types"
|
||||
|
||||
export const modalIdToPropsMap = {} as Record<string, ModalProps>
|
||||
export const useModalStack = (options?: ModalStackOptions) => {
|
||||
const id = useId()
|
||||
const currentCount = useRef(0)
|
||||
|
|
@ -77,3 +79,5 @@ export const useDismissAllWhenRouterChange = () => {
|
|||
actions.dismissAll()
|
||||
}, [pathname])
|
||||
}
|
||||
|
||||
export const useCurrentModal = () => useContext(CurrentModalContext)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export * from "./context"
|
||||
export * from "./helper"
|
||||
export * from "./hooks"
|
||||
export * from "./modal"
|
||||
export * from "./provider"
|
||||
export * from "./types"
|
||||
|
|
|
|||
|
|
@ -15,36 +15,17 @@ import {
|
|||
useRef,
|
||||
} from "react"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
import { useShallow } from "zustand/react/shallow"
|
||||
|
||||
import { Divider } from "../../divider"
|
||||
import { modalMontionConfig } from "./constants"
|
||||
import { modalStackAtom } from "./atom"
|
||||
import { MODAL_STACK_Z_INDEX, modalMontionConfig } from "./constants"
|
||||
import type {
|
||||
CurrentModalContentProps,
|
||||
ModalContentPropsInternal,
|
||||
} from "./context"
|
||||
import { CurrentModalContext, modalStackAtom } from "./context"
|
||||
import { CurrentModalContext } from "./context"
|
||||
import type { ModalProps } from "./types"
|
||||
|
||||
const DialogOverlay = ({
|
||||
onClick,
|
||||
zIndex,
|
||||
}: {
|
||||
onClick?: () => void
|
||||
zIndex?: number
|
||||
}) => (
|
||||
<Dialog.Overlay asChild>
|
||||
<m.div
|
||||
onClick={onClick}
|
||||
className="fixed inset-0 z-[11] bg-zinc-50/80 dark:bg-neutral-900/80"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
style={{ zIndex }}
|
||||
/>
|
||||
</Dialog.Overlay>
|
||||
)
|
||||
|
||||
export const ModalInternal: Component<{
|
||||
item: ModalProps & { id: string }
|
||||
index: number
|
||||
|
|
@ -73,12 +54,13 @@ export const ModalInternal: Component<{
|
|||
[close],
|
||||
)
|
||||
|
||||
const { opaque, overlay: defaultOverlay } = useUIStore(
|
||||
useShallow((state) => ({
|
||||
overlay: state.modalOverlay,
|
||||
opaque: state.modalOpaque,
|
||||
})),
|
||||
)
|
||||
// const { opaque, overlay: defaultOverlay } = useUIStore(
|
||||
// useShallow((state) => ({
|
||||
// overlay: state.modalOverlay,
|
||||
// opaque: state.modalOpaque,
|
||||
// })),
|
||||
// )
|
||||
const opaque = useUIStore((state) => state.modalOpaque)
|
||||
|
||||
const {
|
||||
CustomModalComponent,
|
||||
|
|
@ -90,10 +72,10 @@ export const ModalInternal: Component<{
|
|||
wrapper: Wrapper = Fragment,
|
||||
max,
|
||||
icon,
|
||||
overlay = defaultOverlay,
|
||||
|
||||
draggable = false,
|
||||
} = item
|
||||
const modalStyle = useMemo(() => ({ zIndex: 99 + index }), [index])
|
||||
const zIndexStyle = useMemo(() => ({ zIndex: MODAL_STACK_Z_INDEX + index }), [index])
|
||||
const dismiss = useCallback(
|
||||
(e: SyntheticEvent) => {
|
||||
e.stopPropagation()
|
||||
|
|
@ -180,8 +162,6 @@ export const ModalInternal: Component<{
|
|||
<Wrapper>
|
||||
<Dialog.Root open onOpenChange={onClose}>
|
||||
<Dialog.Portal>
|
||||
{overlay && <DialogOverlay zIndex={19} />}
|
||||
|
||||
<Dialog.DialogTitle className="sr-only">{title}</Dialog.DialogTitle>
|
||||
<Dialog.Content asChild>
|
||||
<div
|
||||
|
|
@ -190,6 +170,7 @@ export const ModalInternal: Component<{
|
|||
modalContainerClassName,
|
||||
)}
|
||||
onClick={clickOutsideToDismiss ? dismiss : undefined}
|
||||
style={zIndexStyle}
|
||||
>
|
||||
<div
|
||||
className={cn("contents", modalClassName)}
|
||||
|
|
@ -204,15 +185,15 @@ export const ModalInternal: Component<{
|
|||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Dialog.Root open onOpenChange={onClose}>
|
||||
<Dialog.Portal>
|
||||
{overlay && <DialogOverlay zIndex={19} />}
|
||||
|
||||
<Dialog.Content asChild>
|
||||
<div
|
||||
ref={edgeElementRef}
|
||||
style={zIndexStyle}
|
||||
className={cn(
|
||||
"center fixed inset-0 z-20 flex",
|
||||
modalContainerClassName,
|
||||
|
|
@ -220,14 +201,14 @@ export const ModalInternal: Component<{
|
|||
onClick={clickOutsideToDismiss ? dismiss : noticeModal}
|
||||
>
|
||||
<m.div
|
||||
style={modalStyle}
|
||||
style={zIndexStyle}
|
||||
{...modalMontionConfig}
|
||||
animate={animateController}
|
||||
className={cn(
|
||||
"relative flex flex-col overflow-hidden rounded-lg",
|
||||
opaque ?
|
||||
"bg-zinc-50 dark:bg-neutral-900" :
|
||||
"bg-zinc-50/80 backdrop-blur-sm dark:bg-neutral-900/80",
|
||||
"bg-theme-modal-background-opaque" :
|
||||
"bg-theme-modal-background backdrop-blur-sm",
|
||||
"shadow-modal p-2",
|
||||
max ?
|
||||
"h-[90vh] w-[90vw]" :
|
||||
|
|
@ -256,7 +237,7 @@ export const ModalInternal: Component<{
|
|||
|
||||
<span>{title}</span>
|
||||
</Dialog.Title>
|
||||
<Dialog.DialogClose className="center p-2" onClick={close}>
|
||||
<Dialog.DialogClose className="center p-2" tabIndex={1} onClick={close}>
|
||||
<i className="i-mgc-close-cute-re" />
|
||||
</Dialog.DialogClose>
|
||||
</div>
|
||||
|
|
@ -265,6 +246,7 @@ export const ModalInternal: Component<{
|
|||
<div className="min-h-0 shrink grow overflow-auto px-4 py-2">
|
||||
{finalChildren}
|
||||
</div>
|
||||
|
||||
</m.div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { useUIStore } from "@renderer/store"
|
||||
import { m } from "framer-motion"
|
||||
|
||||
import { RootPortal } from "../../portal"
|
||||
|
||||
export const ModalOverlay = ({
|
||||
onClick,
|
||||
zIndex,
|
||||
}: {
|
||||
onClick?: () => void
|
||||
zIndex?: number
|
||||
}) => {
|
||||
const modalSettingOverlay = useUIStore((state) => state.modalOverlay)
|
||||
if (!modalSettingOverlay) return null
|
||||
return (
|
||||
<RootPortal>
|
||||
<m.div
|
||||
id="modal-overlay"
|
||||
onClick={onClick}
|
||||
className="fixed inset-0 z-[11] bg-zinc-50/80 dark:bg-neutral-900/80"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
style={{ zIndex }}
|
||||
/>
|
||||
</RootPortal>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,9 +2,11 @@ import { AnimatePresence } from "framer-motion"
|
|||
import { useAtomValue } from "jotai"
|
||||
import type { FC, PropsWithChildren } from "react"
|
||||
|
||||
import { modalStackAtom } from "./context"
|
||||
import { useDismissAllWhenRouterChange } from "./hooks"
|
||||
import { modalStackAtom } from "./atom"
|
||||
import { MODAL_STACK_Z_INDEX } from "./constants"
|
||||
// import { useDismissAllWhenRouterChange } from "./hooks"
|
||||
import { ModalInternal } from "./modal"
|
||||
import { ModalOverlay } from "./overlay"
|
||||
|
||||
export const ModalStackProvider: FC<PropsWithChildren> = ({ children }) => (
|
||||
<>
|
||||
|
|
@ -16,7 +18,8 @@ export const ModalStackProvider: FC<PropsWithChildren> = ({ children }) => (
|
|||
const ModalStack = () => {
|
||||
const stack = useAtomValue(modalStackAtom)
|
||||
|
||||
useDismissAllWhenRouterChange()
|
||||
// Vite HMR issue
|
||||
// useDismissAllWhenRouterChange()
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="popLayout">
|
||||
|
|
@ -28,6 +31,7 @@ const ModalStack = () => {
|
|||
isTop={index === stack.length - 1}
|
||||
/>
|
||||
))}
|
||||
{stack.length > 0 && <ModalOverlay zIndex={MODAL_STACK_Z_INDEX + stack.length - 1} />}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ const SelectContent = React.forwardRef<
|
|||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"shadow-perfect relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"shadow-perfect relative z-[1000] max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,32 @@ declare const routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
status: 200;
|
||||
};
|
||||
};
|
||||
"/wallets/transactions/claim_daily": {
|
||||
$post: {
|
||||
input: {};
|
||||
output: {
|
||||
code: 0;
|
||||
data: {
|
||||
transactionHash: string;
|
||||
};
|
||||
};
|
||||
outputFormat: "json";
|
||||
status: 200;
|
||||
};
|
||||
};
|
||||
"/wallets/transactions/claim_daily_ttl": {
|
||||
$get: {
|
||||
input: {};
|
||||
output: {
|
||||
code: 0;
|
||||
data: {
|
||||
ttl: number;
|
||||
};
|
||||
};
|
||||
outputFormat: "json";
|
||||
status: 200;
|
||||
};
|
||||
};
|
||||
"/wallets": {
|
||||
$get: {
|
||||
input: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useReadonlyRouteSelector } from "@renderer/atoms"
|
||||
import { getReadonlyRoute, useReadonlyRouteSelector } from "@renderer/atoms"
|
||||
import { FeedViewType } from "@renderer/lib/enum"
|
||||
import type { Params } from "react-router-dom"
|
||||
import { useParams, useSearchParams } from "react-router-dom"
|
||||
// '0', '1', '2', '3', '4', '5',
|
||||
const FeedViewTypeValues = (() => {
|
||||
|
|
@ -27,10 +28,17 @@ export const useRouteFeedId = () => {
|
|||
return feedId
|
||||
}
|
||||
|
||||
export const useRouteParms = () => {
|
||||
const params = useParams()
|
||||
const [search] = useSearchParams()
|
||||
const view = useRouteView()
|
||||
const parseRouteParams = (
|
||||
params: Params<any>,
|
||||
search: URLSearchParams,
|
||||
) => {
|
||||
const _view = search.get("view")
|
||||
|
||||
const view = (
|
||||
(_view && FeedViewTypeValues.includes(_view) ?
|
||||
+_view :
|
||||
FeedViewType.Articles) || FeedViewType.Articles
|
||||
)
|
||||
|
||||
let feedId: string | number = params.feedId!
|
||||
|
||||
|
|
@ -47,6 +55,13 @@ export const useRouteParms = () => {
|
|||
category: search.get("category") || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export const useRouteParms = () => {
|
||||
const params = useParams()
|
||||
const [search] = useSearchParams()
|
||||
|
||||
return parseRouteParams(params, search)
|
||||
}
|
||||
const noop = [] as any[]
|
||||
export const useRouteParamsSelector = <T>(
|
||||
selector: (params: {
|
||||
|
|
@ -61,25 +76,11 @@ export const useRouteParamsSelector = <T>(
|
|||
useReadonlyRouteSelector((route) => {
|
||||
const { searchParams, params } = route
|
||||
|
||||
let feedId: string | number = params.feedId!
|
||||
|
||||
// If feedId is a number, it's a FeedViewType
|
||||
if (feedId && FeedViewTypeValues.includes(feedId as string)) {
|
||||
feedId = Number.parseInt(feedId as string)
|
||||
}
|
||||
|
||||
const view = searchParams.get("view")
|
||||
|
||||
const finalView =
|
||||
(view && FeedViewTypeValues.includes(view) ?
|
||||
+view :
|
||||
FeedViewType.Articles) || FeedViewType.Articles
|
||||
|
||||
return selector({
|
||||
entryId: params.entryId || undefined,
|
||||
feedId: params.feedId || undefined,
|
||||
level: searchParams.get("level") || undefined,
|
||||
category: searchParams.get("category") || undefined,
|
||||
view: finalView,
|
||||
})
|
||||
return selector(parseRouteParams(params, searchParams))
|
||||
}, deps)
|
||||
|
||||
export const getRouteParams = () => {
|
||||
const route = getReadonlyRoute()
|
||||
const { searchParams, params } = route
|
||||
return parseRouteParams(params, searchParams)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,347 @@
|
|||
import { compile, pathToRegexp } from "path-to-regexp"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { MissingOptionalParamError, MissingRequiredParamError, parseFullPathParams, parseRegexpPathParams, regexpPathToPath, transformUriPath } from "./path-parser"
|
||||
|
||||
describe("test `transformUriPath()`", () => {
|
||||
test("normal path", () => {
|
||||
expect(transformUriPath("/issues/all")).toMatchInlineSnapshot(
|
||||
`"/issues/all"`,
|
||||
)
|
||||
})
|
||||
|
||||
test("has tailing optional params", () => {
|
||||
const path = transformUriPath("/issues/:id?")
|
||||
expect(path).toMatchInlineSnapshot(`"/issues{/:id}?"`)
|
||||
expect(compile(path)({})).toMatchInlineSnapshot(`"/issues"`)
|
||||
const regexp = pathToRegexp(path)
|
||||
expect(regexp.keys).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"modifier": "?",
|
||||
"name": "id",
|
||||
"pattern": undefined,
|
||||
"prefix": "/",
|
||||
"separator": "/",
|
||||
"suffix": "",
|
||||
},
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
||||
test("has tailing optional params and with value", () => {
|
||||
const path = transformUriPath("/issues/:id?")
|
||||
|
||||
expect(
|
||||
compile(path)({
|
||||
id: "1111111",
|
||||
}),
|
||||
).toMatchInlineSnapshot(`"/issues/1111111"`)
|
||||
const regexp = pathToRegexp(path)
|
||||
expect(regexp.keys).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"modifier": "?",
|
||||
"name": "id",
|
||||
"pattern": undefined,
|
||||
"prefix": "/",
|
||||
"separator": "/",
|
||||
"suffix": "",
|
||||
},
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
||||
test("has many optional params", () => {
|
||||
const path = transformUriPath("/issue/:user/:repo/:state?/:labels?")
|
||||
expect(path).toMatchInlineSnapshot(
|
||||
`"/issue{/:user}{/:repo}{/:state}?{/:labels}?"`,
|
||||
)
|
||||
expect(
|
||||
compile(path)({
|
||||
user: "user",
|
||||
repo: "repo",
|
||||
}),
|
||||
).toMatchInlineSnapshot(`"/issue/user/repo"`)
|
||||
expect(pathToRegexp(path).keys).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"modifier": "",
|
||||
"name": "user",
|
||||
"pattern": undefined,
|
||||
"prefix": "/",
|
||||
"separator": "/",
|
||||
"suffix": "",
|
||||
},
|
||||
{
|
||||
"modifier": "",
|
||||
"name": "repo",
|
||||
"pattern": undefined,
|
||||
"prefix": "/",
|
||||
"separator": "/",
|
||||
"suffix": "",
|
||||
},
|
||||
{
|
||||
"modifier": "?",
|
||||
"name": "state",
|
||||
"pattern": undefined,
|
||||
"prefix": "/",
|
||||
"separator": "/",
|
||||
"suffix": "",
|
||||
},
|
||||
{
|
||||
"modifier": "?",
|
||||
"name": "labels",
|
||||
"pattern": undefined,
|
||||
"prefix": "/",
|
||||
"separator": "/",
|
||||
"suffix": "",
|
||||
},
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
||||
test("catch all path", () => {
|
||||
const path = transformUriPath("/:path{.+}?")
|
||||
expect(path).toMatchInlineSnapshot(`"{/:path}*"`)
|
||||
expect(
|
||||
compile(path)({
|
||||
path: ["a", "b", "c"],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`"/a/b/c"`)
|
||||
})
|
||||
|
||||
test("catch all path with leading route", () => {
|
||||
const path = transformUriPath("/issues/:path{.+}?")
|
||||
expect(path).toMatchInlineSnapshot(`"/issues{/:path}*"`)
|
||||
expect(
|
||||
compile(path)({
|
||||
path: ["a", "b", "c"],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`"/issues/a/b/c"`)
|
||||
})
|
||||
|
||||
test("catch all path but value is query search string", () => {
|
||||
const path = transformUriPath("/issues/:path{.+}?")
|
||||
expect(path).toMatchInlineSnapshot(`"/issues{/:path}*"`)
|
||||
expect(
|
||||
compile(path)({
|
||||
path: ["a=1&b=2"],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`"/issues/a%3D1%26b%3D2"`)
|
||||
})
|
||||
|
||||
test("catch all route via `*`", () => {
|
||||
expect(transformUriPath("/issues/*")).toMatchInlineSnapshot(`"/issues{/:__catchAll__}"`)
|
||||
expect(transformUriPath("*")).toMatchInlineSnapshot(`"{/:__catchAll__}"`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("test `regexpPathToPath()`", () => {
|
||||
test("normal path", () => {
|
||||
expect(regexpPathToPath("/issues/all", {})).toMatchInlineSnapshot(
|
||||
`"/issues/all"`,
|
||||
)
|
||||
})
|
||||
test("path with optional params", () => {
|
||||
expect(regexpPathToPath("/issues/:id?", {})).toMatchInlineSnapshot(
|
||||
`"/issues"`,
|
||||
)
|
||||
expect(regexpPathToPath("/issues/:id?", { id: "1" })).toMatchInlineSnapshot(
|
||||
`"/issues/1"`,
|
||||
)
|
||||
})
|
||||
|
||||
test("path with many optional params", () => {
|
||||
expect(
|
||||
regexpPathToPath("/issue/:user/:repo/:state?/:labels?", {
|
||||
user: "rssnext",
|
||||
repo: "follow",
|
||||
}),
|
||||
).toMatchInlineSnapshot(`"/issue/rssnext/follow"`)
|
||||
})
|
||||
|
||||
test("path with many optional params, but when using the optional parameter(s) after the optional parameter(s), the previous optional parameter(s) is/are not filled in.", () => {
|
||||
expect(
|
||||
() => regexpPathToPath("/issue/:user/:repo/:state?/:labels?", {
|
||||
user: "rssnext",
|
||||
repo: "follow",
|
||||
labels: "rss",
|
||||
}),
|
||||
).toThrowError(MissingOptionalParamError)
|
||||
})
|
||||
test("path with many optional params, but when using the optional parameter(s) after the optional parameter(s), the previous optional parameter(s) is/are not filled in.", () => {
|
||||
expect(
|
||||
() => regexpPathToPath("/ranking/:rid?/:day?/:arc_type?/:disableEmbed?", {
|
||||
day: "1",
|
||||
}),
|
||||
).toThrowError(MissingOptionalParamError)
|
||||
})
|
||||
|
||||
test("missing required param", () => {
|
||||
expect(() => regexpPathToPath("/issue/:user/:repo/:state?/:labels?", {})).toThrow(MissingRequiredParamError)
|
||||
})
|
||||
|
||||
test("catch all path", () => {
|
||||
expect(regexpPathToPath("*", {
|
||||
catchAll: "a/b/c",
|
||||
})).toMatchInlineSnapshot(`"/a%2Fb%2Fc"`)
|
||||
})
|
||||
|
||||
test("path with many optional params and all inputted", () => {
|
||||
expect(regexpPathToPath("/issue/:user/:repo/:state?/:labels?", {
|
||||
user: "rssnext",
|
||||
repo: "follow",
|
||||
state: "open",
|
||||
labels: "rss",
|
||||
})).toMatchInlineSnapshot(`"/issue/rssnext/follow/open/rss"`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("test `parseRegexpPathParams()`", () => {
|
||||
test("normal path", () => {
|
||||
expect(parseRegexpPathParams("/issues/all")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"array": [],
|
||||
"length": 0,
|
||||
"map": {},
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("path with optional params", () => {
|
||||
expect(parseRegexpPathParams("/issues/:id?")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"array": [
|
||||
{
|
||||
"isCatchAll": false,
|
||||
"name": "id",
|
||||
"optional": true,
|
||||
},
|
||||
],
|
||||
"length": 1,
|
||||
"map": {
|
||||
"id": {
|
||||
"isCatchAll": false,
|
||||
"name": "id",
|
||||
"optional": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("path with many optional params", () => {
|
||||
expect(parseRegexpPathParams("/issue/:user/:repo/:state?/:labels?")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"array": [
|
||||
{
|
||||
"isCatchAll": false,
|
||||
"name": "user",
|
||||
"optional": false,
|
||||
},
|
||||
{
|
||||
"isCatchAll": false,
|
||||
"name": "repo",
|
||||
"optional": false,
|
||||
},
|
||||
{
|
||||
"isCatchAll": false,
|
||||
"name": "state",
|
||||
"optional": true,
|
||||
},
|
||||
{
|
||||
"isCatchAll": false,
|
||||
"name": "labels",
|
||||
"optional": true,
|
||||
},
|
||||
],
|
||||
"length": 4,
|
||||
"map": {
|
||||
"labels": {
|
||||
"isCatchAll": false,
|
||||
"name": "labels",
|
||||
"optional": true,
|
||||
},
|
||||
"repo": {
|
||||
"isCatchAll": false,
|
||||
"name": "repo",
|
||||
"optional": false,
|
||||
},
|
||||
"state": {
|
||||
"isCatchAll": false,
|
||||
"name": "state",
|
||||
"optional": true,
|
||||
},
|
||||
"user": {
|
||||
"isCatchAll": false,
|
||||
"name": "user",
|
||||
"optional": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("catch all path", () => {
|
||||
expect(parseRegexpPathParams("/:path{.+}?")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"array": [
|
||||
{
|
||||
"isCatchAll": true,
|
||||
"name": "path",
|
||||
"optional": true,
|
||||
},
|
||||
],
|
||||
"length": 1,
|
||||
"map": {
|
||||
"path": {
|
||||
"isCatchAll": true,
|
||||
"name": "path",
|
||||
"optional": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("catch all path via `*`", () => {
|
||||
expect(parseRegexpPathParams("*")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"array": [
|
||||
{
|
||||
"isCatchAll": true,
|
||||
"name": "__catchAll__",
|
||||
"optional": false,
|
||||
},
|
||||
],
|
||||
"length": 1,
|
||||
"map": {
|
||||
"__catchAll__": {
|
||||
"isCatchAll": true,
|
||||
"name": "__catchAll__",
|
||||
"optional": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("test `parseFullPathParams()`", () => {
|
||||
test("case 1", () => {
|
||||
expect(parseFullPathParams("/user/123344", "/user/:id")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "123344",
|
||||
}
|
||||
`)
|
||||
})
|
||||
test("case 2", () => {
|
||||
expect(parseFullPathParams("/build/wangqiru/ttrss", "/build/:user/:name")).toMatchInlineSnapshot(`
|
||||
{
|
||||
"name": "ttrss",
|
||||
"user": "wangqiru",
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import type { CompileOptions } from "path-to-regexp"
|
||||
import { compile, pathToRegexp } from "path-to-regexp"
|
||||
|
||||
const CATCH_ALL_GROUP_KEY = "__catchAll__"
|
||||
export function transformUriPath(uri: string): string {
|
||||
let result = ""
|
||||
const parts = uri.split("/")
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part) continue
|
||||
if (part.startsWith(":")) {
|
||||
if (part.includes("{") && part.includes("}")) {
|
||||
result += `{/:${part.slice(1, part.indexOf("{"))}}*`
|
||||
} else if (part.endsWith("?")) {
|
||||
result += `{/:${part.slice(1, -1)}}?`
|
||||
} else {
|
||||
result += `{/:${part.slice(1)}}`
|
||||
}
|
||||
} else if (part === "*") {
|
||||
result += `{/:${CATCH_ALL_GROUP_KEY}}`
|
||||
} else {
|
||||
result += `/${part}`
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export class MissingOptionalParamError extends Error {
|
||||
constructor(public param: string) {
|
||||
super(`You used the optional parameter ${param}, but there are other optional parameters before the optional parameter ${param} that you did not fill in.`)
|
||||
}
|
||||
}
|
||||
|
||||
export class MissingRequiredParamError extends Error {
|
||||
constructor(param: string) {
|
||||
super(`Missing required param ${param}`)
|
||||
}
|
||||
}
|
||||
export const regexpPathToPath = (
|
||||
regexpPath: string,
|
||||
params: {
|
||||
catchAll?: string
|
||||
[key: string]: string | string[] | undefined
|
||||
},
|
||||
options?: Omit<CompileOptions, "validate">,
|
||||
): string => {
|
||||
const transformedPath = transformUriPath(regexpPath)
|
||||
|
||||
const paramsKeys = pathToRegexp(transformedPath).keys
|
||||
const inputtedKeys = new Set(Object.keys(params))
|
||||
let prevKeyIsOptional = false
|
||||
let prevKeyIsInputted = false
|
||||
for (const key of paramsKeys) {
|
||||
if (key.name === CATCH_ALL_GROUP_KEY) {
|
||||
params[CATCH_ALL_GROUP_KEY] = params.catchAll
|
||||
break
|
||||
}
|
||||
const isOptional = key.modifier === "?"
|
||||
const userInputted = inputtedKeys.has(key.name)
|
||||
if (!isOptional && !userInputted) {
|
||||
throw new MissingRequiredParamError(key.name)
|
||||
}
|
||||
|
||||
if (prevKeyIsOptional && !isOptional) {
|
||||
throw new Error("Required param after optional param")
|
||||
}
|
||||
|
||||
if (prevKeyIsOptional && !prevKeyIsInputted && userInputted) {
|
||||
throw new MissingOptionalParamError(key.name)
|
||||
}
|
||||
|
||||
prevKeyIsOptional = isOptional
|
||||
prevKeyIsInputted = userInputted
|
||||
}
|
||||
return compile(transformedPath, {
|
||||
validate: false,
|
||||
|
||||
...options,
|
||||
})(params)
|
||||
}
|
||||
|
||||
export type PathParams = {
|
||||
name: string
|
||||
optional: boolean
|
||||
isCatchAll: boolean
|
||||
|
||||
}
|
||||
export const parseRegexpPathParams = (regexpPath: string) => {
|
||||
const transformedPath = transformUriPath(regexpPath)
|
||||
const array: PathParams[] = []
|
||||
|
||||
for (const item of pathToRegexp(transformedPath).keys) {
|
||||
array.push({
|
||||
name: item.name,
|
||||
optional: item.modifier === "?" || item.modifier === "*",
|
||||
isCatchAll: item.name === CATCH_ALL_GROUP_KEY || item.modifier === "*",
|
||||
})
|
||||
}
|
||||
|
||||
const map = array.reduce((acc, { name, optional, isCatchAll }) => {
|
||||
acc[name] = { name, optional, isCatchAll }
|
||||
return acc
|
||||
}, {}) as Record<string, PathParams>
|
||||
return {
|
||||
array,
|
||||
map,
|
||||
length: array.length,
|
||||
}
|
||||
}
|
||||
export const parseFullPathParams = (path: string, regexpPath: string) => {
|
||||
// path: /user/123344
|
||||
// regexpPath: /user/:id
|
||||
// return {id: '123344'}
|
||||
const transformedPath = transformUriPath(regexpPath)
|
||||
const { keys } = pathToRegexp(transformedPath)
|
||||
const result = pathToRegexp(transformedPath).exec(path)
|
||||
if (!result) return {}
|
||||
return keys.reduce((acc, key, index) => {
|
||||
acc[key.name] = result[index + 1]
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { getSidebarActiveView } from "@renderer/atoms/sidebar"
|
||||
import { StyledButton } from "@renderer/components/ui/button"
|
||||
import { CopyButton } from "@renderer/components/ui/code-highlighter"
|
||||
import { Form, FormItem, FormLabel } from "@renderer/components/ui/form"
|
||||
import { Input } from "@renderer/components/ui/input"
|
||||
import { Markdown } from "@renderer/components/ui/markdown"
|
||||
import { useModalStack } from "@renderer/components/ui/modal"
|
||||
import type { FeedViewType } from "@renderer/lib/enum"
|
||||
import {
|
||||
MissingOptionalParamError,
|
||||
parseFullPathParams,
|
||||
parseRegexpPathParams,
|
||||
regexpPathToPath,
|
||||
} from "@renderer/lib/path-parser"
|
||||
import type { FC } from "react"
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef } from "react"
|
||||
import type { UseFormReturn } from "react-hook-form"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
import { FeedForm } from "./feed-form"
|
||||
import type { RSSHubRoute } from "./types"
|
||||
import { normalizeRSSHubParameters } from "./utils"
|
||||
|
||||
const FeedMaintainers = ({ maintainers }: { maintainers?: string[] }) => {
|
||||
if (!maintainers || maintainers.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-2 text-sm">
|
||||
<p>
|
||||
The maintainers of this feed are as follows. If you have any questions
|
||||
or suggestions, you can contact them.
|
||||
</p>
|
||||
<ul className="flex flex-wrap items-center gap-4">
|
||||
{maintainers.map((maintainer) => (
|
||||
<a
|
||||
href={`https://github.com/${maintainer}`}
|
||||
key={maintainer}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="inline-flex cursor-pointer items-center text-theme-foreground/50 duration-200 hover:text-theme-accent"
|
||||
>
|
||||
@
|
||||
{maintainer}
|
||||
<i className="i-mgc-external-link-cute-re ml-0.5" />
|
||||
</a>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FeedDescription = ({ description }: { description?: string }) => {
|
||||
if (!description) {
|
||||
return <p className="mb-4">This feed does not have a description.</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
The description of this feed is as follows, and you can fill out the
|
||||
parameter form with the relevant information.
|
||||
</p>
|
||||
<Markdown className="my-4 w-full max-w-full cursor-text select-text">
|
||||
{description}
|
||||
</Markdown>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const DiscoverFeedForm = ({
|
||||
route,
|
||||
routePrefix,
|
||||
}: {
|
||||
route: RSSHubRoute
|
||||
routePrefix
|
||||
}) => {
|
||||
const keys = parseRegexpPathParams(route.path)
|
||||
|
||||
const formPlaceholder = useMemo<Record<string, string>>(() => {
|
||||
if (!route.example) return {}
|
||||
return parseFullPathParams(
|
||||
route.example.replace(`/${routePrefix}`, ""),
|
||||
route.path,
|
||||
)
|
||||
}, [route.example, route.path, routePrefix])
|
||||
const dynamicFormSchema = useMemo(
|
||||
() =>
|
||||
z.object({
|
||||
...Object.fromEntries(
|
||||
keys.array.map((keyItem) => [
|
||||
keyItem.name,
|
||||
keyItem.optional ? z.string().optional() : z.string().min(1),
|
||||
]),
|
||||
),
|
||||
}),
|
||||
[keys],
|
||||
)
|
||||
const form = useForm<z.infer<typeof dynamicFormSchema>>({
|
||||
resolver: zodResolver(dynamicFormSchema),
|
||||
defaultValues: {},
|
||||
mode: "all",
|
||||
}) as UseFormReturn<any>
|
||||
|
||||
const { present, dismissAll } = useModalStack()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data) => {
|
||||
try {
|
||||
// Delete empty string values
|
||||
const nextData = { ...data }
|
||||
for (const key in nextData) {
|
||||
if (nextData[key] === "") {
|
||||
delete nextData[key]
|
||||
}
|
||||
}
|
||||
|
||||
const fillRegexpPath = regexpPathToPath(route.path, nextData)
|
||||
const url = `rsshub://${routePrefix}${fillRegexpPath}`
|
||||
const defaultView = getSidebarActiveView() as FeedViewType
|
||||
|
||||
present({
|
||||
title: "Add follow",
|
||||
content: () => (
|
||||
<FeedForm
|
||||
asWidget
|
||||
url={url}
|
||||
defaultView={defaultView}
|
||||
onSuccess={dismissAll}
|
||||
/>
|
||||
),
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof MissingOptionalParamError) {
|
||||
toast.error(err.message)
|
||||
const idx = keys.array.findIndex((item) => item.name === err.param)
|
||||
|
||||
form.setFocus(keys.array[idx === 0 ? 0 : idx - 1].name)
|
||||
}
|
||||
}
|
||||
},
|
||||
[dismissAll, form, keys.array, present, route.path, routePrefix],
|
||||
)
|
||||
|
||||
const formElRef = useRef<HTMLFormElement>(null)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const $form = formElRef.current
|
||||
if (!$form) return
|
||||
$form.querySelectorAll("input")[0]?.focus()
|
||||
}, [formElRef])
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
ref={formElRef}
|
||||
>
|
||||
{keys.array.map((keyItem) => {
|
||||
const parameters = normalizeRSSHubParameters(
|
||||
route.parameters[keyItem.name],
|
||||
)
|
||||
|
||||
return (
|
||||
<FormItem key={keyItem.name} className="flex flex-col space-y-2">
|
||||
<FormLabel>
|
||||
{keyItem.name}
|
||||
{!keyItem.optional && (
|
||||
<sup className="ml-1 align-sub text-red-500">*</sup>
|
||||
)}
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...form.register(keyItem.name)}
|
||||
placeholder={parameters?.default ?? formPlaceholder[keyItem.name]}
|
||||
defaultValue={parameters?.default || void 0}
|
||||
/>
|
||||
{!!parameters && (
|
||||
<p className="text-xs text-theme-foreground/50">
|
||||
{parameters.description}
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)
|
||||
})}
|
||||
|
||||
<PreviewUrl
|
||||
watch={form.watch}
|
||||
path={route.path}
|
||||
routePrefix={`rsshub://${routePrefix}`}
|
||||
/>
|
||||
|
||||
<FeedMaintainers maintainers={route.maintainers} />
|
||||
|
||||
<div className="sticky bottom-0 -mt-4 flex w-full translate-y-3 justify-end bg-theme-modal-background-opaque py-3">
|
||||
<StyledButton disabled={!form.formState.isValid} type="submit">
|
||||
Preview
|
||||
</StyledButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
const PreviewUrl: FC<{
|
||||
watch: UseFormReturn<any>["watch"]
|
||||
path: string
|
||||
routePrefix: string
|
||||
}> = ({ watch, path, routePrefix }) => {
|
||||
const data = watch()
|
||||
|
||||
const fullPath = useMemo(() => {
|
||||
try {
|
||||
return regexpPathToPath(path, data)
|
||||
} catch (err: unknown) {
|
||||
console.info((err as Error).message)
|
||||
return path
|
||||
}
|
||||
}, [path, data])
|
||||
|
||||
const renderedPath = `${routePrefix}${fullPath}`
|
||||
return (
|
||||
<div className="group relative min-w-0 py-2">
|
||||
<pre className="w-full whitespace-pre-line break-words text-xs text-theme-foreground/40">
|
||||
{renderedPath}
|
||||
</pre>
|
||||
<CopyButton
|
||||
value={renderedPath}
|
||||
className="absolute right-0 top-0 opacity-0 duration-200 group-hover:opacity-100"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -24,12 +24,13 @@ import { ViewSelectContent } from "@renderer/components/view-select-content"
|
|||
import { useBizQuery } from "@renderer/hooks"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { tipcClient } from "@renderer/lib/client"
|
||||
import { nextFrame } from "@renderer/lib/dom"
|
||||
import { FeedViewType } from "@renderer/lib/enum"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { Queries } from "@renderer/queries"
|
||||
import { useFeed } from "@renderer/queries/feed"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useEffect } from "react"
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
|
@ -134,6 +135,11 @@ export const FeedForm: Component<{
|
|||
Queries.subscription.categories(Number.parseInt(form.watch("view"))),
|
||||
)
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
useEffect(() => {
|
||||
if (feed.isSuccess) nextFrame(() => buttonRef.current?.focus())
|
||||
}, [feed.isSuccess])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -248,6 +254,7 @@ export const FeedForm: Component<{
|
|||
|
||||
<div className="flex flex-1 items-end justify-end">
|
||||
<StyledButton
|
||||
ref={buttonRef}
|
||||
type="submit"
|
||||
isLoading={followMutation.isPending}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import type { RecommendationItem } from "@renderer/models"
|
||||
import { DiscoverFeedForm, FeedDescription } from "./content-components"
|
||||
import type { RSSHubRoute } from "./types"
|
||||
|
||||
export const RecommendationContent = ({
|
||||
route,
|
||||
routePrefix,
|
||||
}: {
|
||||
route: RecommendationItem["routes"][string]
|
||||
route: RSSHubRoute
|
||||
routePrefix: string
|
||||
}) => (
|
||||
<>
|
||||
rsshub:/
|
||||
{route.path}
|
||||
</>
|
||||
<div className="w-full min-w-[550px] max-w-[700px]">
|
||||
<FeedDescription description={route.description} />
|
||||
|
||||
<DiscoverFeedForm route={route} routePrefix={routePrefix} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import { SiteIcon } from "@renderer/components/site-icon"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@renderer/components/ui/card"
|
||||
import { useModalStack } from "@renderer/components/ui/modal/stacked/hooks"
|
||||
import type { FC } from "react"
|
||||
import { memo } from "react"
|
||||
|
||||
import { RecommendationContent } from "./recommendation-content"
|
||||
import type { RSSHubRouteDeclaration } from "./types"
|
||||
|
||||
interface RecommendationCardProps {
|
||||
data: RSSHubRouteDeclaration
|
||||
routePrefix: string
|
||||
}
|
||||
export const RecommendationCard: FC<RecommendationCardProps> = memo(({ data, routePrefix }) => {
|
||||
const { present } = useModalStack()
|
||||
return (
|
||||
<Card className="shadow-none">
|
||||
<CardHeader className="p-5 pb-3">
|
||||
<CardTitle className="flex items-center text-base">
|
||||
<SiteIcon url={`https://${data.url}`} />
|
||||
{data.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-5 pt-0">
|
||||
<CardDescription>
|
||||
<ul className="space-y-1">
|
||||
{Object.keys(data.routes).map((route) => (
|
||||
<li
|
||||
key={route}
|
||||
className="duration-200 hover:font-medium hover:text-theme-foreground-hover"
|
||||
onClick={() => {
|
||||
present({
|
||||
content: () => (
|
||||
<RecommendationContent routePrefix={routePrefix} route={data.routes[route]} />
|
||||
),
|
||||
icon: (
|
||||
<SiteIcon
|
||||
className="size-4"
|
||||
url={`https://${data.url}`}
|
||||
/>
|
||||
),
|
||||
title: `${data.name} - ${data.routes[route].name}`,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{data.routes[route].name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -1,69 +1,35 @@
|
|||
import { SiteIcon } from "@renderer/components/site-icon"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@renderer/components/ui/card"
|
||||
import { useModalStack } from "@renderer/components/ui/modal/stacked/hooks"
|
||||
import { LoadingCircle } from "@renderer/components/ui/loading"
|
||||
import { useBizQuery } from "@renderer/hooks"
|
||||
import { Queries } from "@renderer/queries"
|
||||
|
||||
import { RecommendationContent } from "./recommendation-content"
|
||||
import { RecommendationCard } from "./recommendations-card"
|
||||
|
||||
export function Recommendations() {
|
||||
const rsshubPopular = useBizQuery(Queries.discover.rsshubCategory({
|
||||
category: "popular",
|
||||
}))
|
||||
const { present } = useModalStack()
|
||||
const rsshubPopular = useBizQuery(
|
||||
Queries.discover.rsshubCategory({
|
||||
category: "popular",
|
||||
}),
|
||||
)
|
||||
|
||||
if (rsshubPopular.isLoading) {
|
||||
return <LoadingCircle className="mt-16" size="medium" />
|
||||
}
|
||||
|
||||
const { data } = rsshubPopular
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<div className="text-center text-lg font-medium">Popular</div>
|
||||
{rsshubPopular.data && (
|
||||
<div className="mt-4 grid grid-cols-3 gap-4">
|
||||
{Object.keys(rsshubPopular.data).map((key) => (
|
||||
<Card key={key}>
|
||||
<CardHeader className="p-5 pb-3">
|
||||
<CardTitle className="flex items-center text-base">
|
||||
<SiteIcon
|
||||
url={`https://${rsshubPopular.data[key].url}`}
|
||||
/>
|
||||
{rsshubPopular.data[key].name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-5 pt-0">
|
||||
<CardDescription>
|
||||
<ul className="space-y-1">
|
||||
{Object.keys(rsshubPopular.data[key].routes).map((route) => (
|
||||
<li
|
||||
key={route}
|
||||
className="transition-colors hover:font-medium hover:text-zinc-800"
|
||||
onClick={() => {
|
||||
present({
|
||||
content: () => <RecommendationContent route={rsshubPopular.data[key].routes[route]} />,
|
||||
icon: (
|
||||
<SiteIcon
|
||||
className="size-4"
|
||||
url={`https://${rsshubPopular.data[key].url}`}
|
||||
/>
|
||||
),
|
||||
title: `${rsshubPopular.data[key].name} - ${rsshubPopular.data[key].routes[route].name}`,
|
||||
clickOutsideToDismiss: true,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{rsshubPopular.data[key].routes[route].name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center text-lg font-bold">Popular</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-3 gap-4 px-3">
|
||||
{Object.keys(data).map((key) => (
|
||||
<RecommendationCard key={key} data={data[key]} routePrefix={key} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
export type RSSHubRouteType = Record<string, RSSHubRouteDeclaration>
|
||||
export interface RSSHubRouteDeclaration {
|
||||
routes: Routes
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
type Routes = Record<string, RSSHubRoute>
|
||||
export type RSSHubParameterObject = {
|
||||
description: string
|
||||
default: string | null
|
||||
options?: {
|
||||
label: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export type RSSHubParameter = string | RSSHubParameterObject
|
||||
export type RSSHubRoute = {
|
||||
path: string
|
||||
categories: string[]
|
||||
example: string
|
||||
parameters: Record<string, RSSHubParameter>
|
||||
name: string
|
||||
maintainers: string[]
|
||||
location: string
|
||||
description: string
|
||||
|
||||
// features: Features
|
||||
// radar: RadarItem[]
|
||||
}
|
||||
|
||||
// interface Features {
|
||||
// requireConfig: boolean
|
||||
// requirePuppeteer: boolean
|
||||
// antiCrawler: boolean
|
||||
// supportBT: boolean
|
||||
// supportPodcast: boolean
|
||||
// supportScihub: boolean
|
||||
// }
|
||||
// interface RadarItem {
|
||||
// source: string[]
|
||||
// target: string
|
||||
// }
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import type { RSSHubParameter, RSSHubParameterObject } from "./types"
|
||||
|
||||
export const normalizeRSSHubParameters = (parameters: RSSHubParameter): RSSHubParameterObject | null => parameters ? typeof parameters === "string" ? { description: parameters, default: null } : parameters : null
|
||||
|
|
@ -27,7 +27,7 @@ export function ListItem({
|
|||
if (!entry) return <ReactVirtuosoItemPlaceholder />
|
||||
|
||||
return (
|
||||
<div className={cn("relative flex py-3 pl-3 pr-2", !asRead && "before:absolute before:-left-0.5 before:top-[18px] before:block before:size-2 before:rounded-full before:bg-blue-500")}>
|
||||
<div className={cn("relative flex py-3 pl-3 pr-2", !asRead && "before:absolute before:-left-0.5 before:top-[18px] before:block before:size-2 before:rounded-full before:bg-theme-accent")}>
|
||||
<FeedIcon feed={entry.feeds} entry={entry.entries} />
|
||||
<div className="-mt-0.5 line-clamp-4 flex-1 text-sm leading-tight">
|
||||
<div className={cn("flex gap-1 text-[10px] font-bold", asRead ? "text-zinc-400" : "text-zinc-500")}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { getReadonlyRoute } from "@renderer/atoms"
|
||||
import { useSidebarActiveView } from "@renderer/atoms/sidebar"
|
||||
import { Logo } from "@renderer/components/icons/logo"
|
||||
import { ActionButton } from "@renderer/components/ui/button"
|
||||
import { ProfileButton } from "@renderer/components/user-button"
|
||||
|
|
@ -11,7 +12,7 @@ import { clamp, cn } from "@renderer/lib/utils"
|
|||
import { useWheel } from "@use-gesture/react"
|
||||
import { m, useSpring } from "framer-motion"
|
||||
import { Lethargy } from "lethargy"
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import { useCallback, useRef } from "react"
|
||||
import { isHotkeyPressed, useHotkeys } from "react-hotkeys-hook"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ const useBackHome = (active: number) => {
|
|||
export function FeedColumn() {
|
||||
const carouselRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [active, setActive_] = useState(0)
|
||||
const [active, setActive_] = useSidebarActiveView()
|
||||
const spring = useSpring(0, {
|
||||
stiffness: 700,
|
||||
damping: 40,
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ export function FeedList({
|
|||
to="/discover"
|
||||
className="-mt-36 flex h-full flex-1 flex-col items-center justify-center gap-2"
|
||||
>
|
||||
<i className="i-mgc-add-cute-re text-3xl " />
|
||||
<i className="i-mgc-add-cute-re text-3xl" />
|
||||
Add some feeds
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@
|
|||
|
||||
--fo-item-active: theme(colors.native.active/0.4);
|
||||
--fo-item-hover: theme(colors.native.active/0.2);
|
||||
|
||||
--fo-modal-background: theme(colors.zinc.50/0.8);
|
||||
--fo-modal-background-opaque: theme(colors.zinc.50);
|
||||
|
||||
--fo-text-primary-hover: 0, 0%, 14.9%;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
|
|
@ -31,4 +36,9 @@
|
|||
|
||||
--fo-text-primary: 0, 0%, 80%;
|
||||
--fo-background: theme(colors.background);
|
||||
|
||||
--fo-modal-background: theme(colors.neutral.900/0.8);
|
||||
--fo-modal-background-opaque: theme(colors.neutral.900);
|
||||
|
||||
--fo-text-primary-hover: 240, 4.9%, 83.9%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default resolveConfig({
|
|||
|
||||
theme: {
|
||||
// https://uicolors.app/create
|
||||
accent: {
|
||||
"accent": {
|
||||
DEFAULT: "#ff5c00",
|
||||
50: "#fff7ec",
|
||||
100: "#ffeed3",
|
||||
|
|
@ -85,24 +85,31 @@ export default resolveConfig({
|
|||
950: "#461304",
|
||||
},
|
||||
|
||||
vibrancyFg: "hsl(var(--fo-vibrancy-foreground), <alpha-value>)",
|
||||
vibrancyBg: "var(--fo-vibrancy-background)",
|
||||
"vibrancyFg": "hsl(var(--fo-vibrancy-foreground), <alpha-value>)",
|
||||
"vibrancyBg": "var(--fo-vibrancy-background)",
|
||||
|
||||
item: {
|
||||
"item": {
|
||||
active: "var(--fo-item-active)",
|
||||
hover: "var(--fo-item-hover)",
|
||||
},
|
||||
|
||||
tooltip: {
|
||||
"tooltip": {
|
||||
background: "hsl(var(--fo-tooltip-background), <alpha-value>)",
|
||||
foreground: "hsl(var(--fo-tooltip-foreground), <alpha-value>)",
|
||||
},
|
||||
|
||||
inactive: "hsl(var(--fo-inactive), <alpha-value>)",
|
||||
disabled: "hsl(var(--fo-disabled), <alpha-value>)",
|
||||
"inactive": "hsl(var(--fo-inactive), <alpha-value>)",
|
||||
"disabled": "hsl(var(--fo-disabled), <alpha-value>)",
|
||||
|
||||
foreground: "hsl(var(--fo-text-primary), <alpha-value>)",
|
||||
background: "var(--fo-background)",
|
||||
"foreground": "hsl(var(--fo-text-primary), <alpha-value>)",
|
||||
"background": "var(--fo-background)",
|
||||
|
||||
"foreground-hover": "hsl(var(--fo-text-primary-hover), <alpha-value>)",
|
||||
|
||||
"modal": {
|
||||
"background": "var(--fo-modal-background)",
|
||||
"background-opaque": "var(--fo-modal-background-opaque)",
|
||||
},
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue