diff --git a/src/main/core/subStoreApi.ts b/src/main/core/subStoreApi.ts new file mode 100644 index 0000000..6f7700f --- /dev/null +++ b/src/main/core/subStoreApi.ts @@ -0,0 +1,14 @@ +import axios from 'axios' +import { subStorePort } from '../resolve/server' + +export async function subStoreSubs(): Promise { + const res = await axios.get(`http://127.0.0.1:${subStorePort}/api/subs`) + + return res.data.data as ISubStoreSub[] +} + +export async function subStoreCollections(): Promise { + const res = await axios.get(`http://127.0.0.1:${subStorePort}/api/collections`) + + return res.data.data as ISubStoreSub[] +} diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 31dde19..ebc2420 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -60,6 +60,7 @@ import { getInterfaces } from '../sys/interface' import { copyEnv } from '../resolve/tray' import { registerShortcut } from '../resolve/shortcut' import { mainWindow } from '..' +import { subStoreCollections, subStoreSubs } from '../core/subStoreApi' function ipcErrorWrapper( // eslint-disable-next-line @typescript-eslint/no-explicit-any fn: (...args: any[]) => Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -168,6 +169,8 @@ export function registerIpcMainHandlers(): void { ) ipcMain.handle('startSubStoreServer', () => ipcErrorWrapper(startSubStoreServer)()) ipcMain.handle('subStorePort', () => subStorePort) + ipcMain.handle('subStoreSubs', () => ipcErrorWrapper(subStoreSubs)()) + ipcMain.handle('subStoreCollections', () => ipcErrorWrapper(subStoreCollections)()) ipcMain.handle('setNativeTheme', (_e, theme) => { setNativeTheme(theme) }) diff --git a/src/renderer/src/pages/profiles.tsx b/src/renderer/src/pages/profiles.tsx index 8c45cad..5639cc8 100644 --- a/src/renderer/src/pages/profiles.tsx +++ b/src/renderer/src/pages/profiles.tsx @@ -12,8 +12,14 @@ import BasePage from '@renderer/components/base/base-page' import ProfileItem from '@renderer/components/profiles/profile-item' import { useProfileConfig } from '@renderer/hooks/use-profile-config' import { useAppConfig } from '@renderer/hooks/use-app-config' -import { getFilePath, readTextFile, subStorePort } from '@renderer/utils/ipc' -import { useEffect, useRef, useState } from 'react' +import { + getFilePath, + readTextFile, + subStoreCollections, + subStorePort, + subStoreSubs +} from '@renderer/utils/ipc' +import { ReactNode, useEffect, useMemo, useRef, useState } from 'react' import { MdContentPaste } from 'react-icons/md' import { DndContext, @@ -27,6 +33,7 @@ import { SortableContext } from '@dnd-kit/sortable' import { FaPlus } from 'react-icons/fa6' import { IoMdRefresh } from 'react-icons/io' import SubStoreIcon from '@renderer/components/base/substore-icon' +import useSWR from 'swr' const Profiles: React.FC = () => { const { @@ -43,11 +50,51 @@ const Profiles: React.FC = () => { const { current, items = [] } = profileConfig || {} const [sortedItems, setSortedItems] = useState(items) const [useProxy, setUseProxy] = useState(false) + const [subStoreImporting, setSubStoreImporting] = useState(false) const [importing, setImporting] = useState(false) const [updating, setUpdating] = useState(false) const [fileOver, setFileOver] = useState(false) const [url, setUrl] = useState('') const sensors = useSensors(useSensor(PointerSensor)) + const { data: subs = [], mutate: mutateSubs } = useSWR( + useSubStore ? 'subStoreSubs' : undefined, + useSubStore ? subStoreSubs : (): undefined => {} + ) + const { data: collections = [], mutate: mutateCollections } = useSWR( + useSubStore ? 'subStoreCollections' : undefined, + useSubStore ? subStoreCollections : (): undefined => {} + ) + const subStoreMenuItems = useMemo(() => { + const items: { icon?: ReactNode; key: string; name: string; divider: boolean }[] = [ + { + key: 'open-substore', + name: '访问 SubStore', + icon: , + divider: Boolean(subs) || Boolean(collections) + } + ] + if (subs) { + subs.forEach((sub, index) => { + items.push({ + key: `sub-${sub.name}`, + name: sub.displayName, + icon: sub.icon ? : null, + divider: index === subs.length - 1 && Boolean(collections) + }) + }) + } + if (collections) { + collections.forEach((sub) => { + items.push({ + key: `collection-${sub.name}`, + name: sub.displayName, + icon: sub.icon ? : null, + divider: false + }) + }) + } + return items + }, [subs, collections]) const handleImport = async (): Promise => { setImporting(true) await addProfileItem({ name: '', type: 'remote', url, useProxy }) @@ -194,19 +241,75 @@ const Profiles: React.FC = () => { 导入 {useSubStore && ( - + + + + { + if (key === 'open-substore') { + const port = await subStorePort() + open(`https://sub-store.vercel.app/subs?api=http://127.0.0.1:${port}`) + } else if (key.toString().startsWith('sub-')) { + setSubStoreImporting(true) + try { + const port = await subStorePort() + const sub = subs.find( + (sub) => sub.name === key.toString().replace('sub-', '') + ) + await addProfileItem({ + name: sub?.displayName ?? '', + type: 'remote', + url: `http://127.0.0.1:${port}/download/${key.toString().replace('sub-', '')}?target=ClashMeta`, + useProxy + }) + } catch (e) { + alert(e) + } finally { + setSubStoreImporting(false) + } + } else if (key.toString().startsWith('collection-')) { + setSubStoreImporting(true) + try { + const port = await subStorePort() + const sub = collections.find( + (sub) => sub.name === key.toString().replace('sub-', '') + ) + await addProfileItem({ + name: sub?.displayName ?? '', + type: 'remote', + url: `http://127.0.0.1:${port}/download/collection/${key.toString().replace('collection-', '')}?target=ClashMeta`, + useProxy + }) + } catch (e) { + alert(e) + } finally { + setSubStoreImporting(false) + } + } + }} + > + {subStoreMenuItems.map((item) => ( + + {item.name} + + ))} + + )} diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 310e5e2..4a8eb96 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -299,6 +299,14 @@ export async function subStorePort(): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('subStorePort')) } +export async function subStoreSubs(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('subStoreSubs')) +} + +export async function subStoreCollections(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('subStoreCollections')) +} + export async function openFile( type: 'profile' | 'override', id: string, diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index d17ba1d..59cd01b 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -399,3 +399,10 @@ interface IProfileItem { useProxy?: boolean extra?: ISubscriptionUserInfo } + +interface ISubStoreSub { + name: string + displayName: sstring + icon?: string + tag?: string[] +}