✨ feat(api): Add getClient API endpoint to retrieve subscription applications
This commit is contained in:
@@ -5,8 +5,7 @@ import Renewal from '@/components/subscribe/renewal';
|
||||
import ResetTraffic from '@/components/subscribe/reset-traffic';
|
||||
import Unsubscribe from '@/components/subscribe/unsubscribe';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { getStat } from '@/services/common/common';
|
||||
import { queryApplicationConfig } from '@/services/user/subscribe';
|
||||
import { getClient, getStat } from '@/services/common/common';
|
||||
import { queryUserSubscribe, resetUserSubscribeToken } from '@/services/user/user';
|
||||
import { getPlatform } from '@/utils/common';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -43,9 +42,9 @@ import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import { toast } from 'sonner';
|
||||
import Subscribe from '../subscribe/page';
|
||||
|
||||
const platforms: (keyof API.ApplicationPlatform)[] = [
|
||||
const platforms: (keyof API.DownloadLink)[] = [
|
||||
'windows',
|
||||
'macos',
|
||||
'mac',
|
||||
'linux',
|
||||
'ios',
|
||||
'android',
|
||||
@@ -70,13 +69,15 @@ export default function Content() {
|
||||
},
|
||||
});
|
||||
const { data: applications } = useQuery({
|
||||
queryKey: ['queryApplicationConfig'],
|
||||
queryKey: ['getClient'],
|
||||
queryFn: async () => {
|
||||
const { data } = await queryApplicationConfig();
|
||||
return data.data?.applications || [];
|
||||
const { data } = await getClient();
|
||||
return data.data?.list || [];
|
||||
},
|
||||
});
|
||||
const [platform, setPlatform] = useState<keyof API.ApplicationPlatform>(getPlatform());
|
||||
const [platform, setPlatform] = useState<keyof API.DownloadLink>(
|
||||
getPlatform() === 'macos' ? 'mac' : (getPlatform() as keyof API.DownloadLink),
|
||||
);
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['getStat'],
|
||||
@@ -123,7 +124,7 @@ export default function Content() {
|
||||
<div className='flex flex-wrap justify-between gap-4'>
|
||||
<Tabs
|
||||
value={platform}
|
||||
onValueChange={(value) => setPlatform(value as keyof API.ApplicationPlatform)}
|
||||
onValueChange={(value) => setPlatform(value as keyof API.DownloadLink)}
|
||||
className='w-full max-w-full md:w-auto'
|
||||
>
|
||||
<TabsList className='flex *:flex-auto'>
|
||||
@@ -133,7 +134,7 @@ export default function Content() {
|
||||
icon={`${
|
||||
{
|
||||
windows: 'mdi:microsoft-windows',
|
||||
macos: 'uil:apple',
|
||||
mac: 'uil:apple',
|
||||
linux: 'uil:linux',
|
||||
ios: 'simple-icons:ios',
|
||||
android: 'uil:android',
|
||||
@@ -324,19 +325,18 @@ export default function Content() {
|
||||
<div className='grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6'>
|
||||
{applications
|
||||
?.filter((application) => {
|
||||
const platformApps = application.platform?.[platform];
|
||||
return platformApps && platformApps.length > 0;
|
||||
// 检查当前平台是否有下载链接或者有 scheme
|
||||
if (!application.download_link && !application.scheme) return false;
|
||||
return (
|
||||
!!application.download_link?.[platform] || !!application.scheme
|
||||
);
|
||||
})
|
||||
.map((application) => {
|
||||
const platformApps = application.platform?.[platform];
|
||||
const app =
|
||||
platformApps?.find((item) => item.is_default) ||
|
||||
platformApps?.[0];
|
||||
if (!app) return null;
|
||||
const downloadUrl = application.download_link?.[platform];
|
||||
|
||||
const handleCopy = (text: string, result: boolean) => {
|
||||
if (result) {
|
||||
const href = getAppSubLink(application.subscribe_type, url);
|
||||
const href = getAppSubLink(url, application.scheme);
|
||||
const showSuccessMessage = () => {
|
||||
toast.success(
|
||||
<>
|
||||
@@ -379,23 +379,32 @@ export default function Content() {
|
||||
/>
|
||||
)}
|
||||
<div className='flex'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='secondary'
|
||||
className='rounded-r-none px-1.5'
|
||||
asChild
|
||||
>
|
||||
<Link href={app.url}>{t('download')}</Link>
|
||||
</Button>
|
||||
|
||||
<CopyToClipboard
|
||||
text={getAppSubLink(application.subscribe_type, url) || url}
|
||||
onCopy={handleCopy}
|
||||
>
|
||||
<Button size='sm' className='rounded-l-none p-2'>
|
||||
{t('import')}
|
||||
{downloadUrl && (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='secondary'
|
||||
className={
|
||||
application.scheme ? 'rounded-r-none px-1.5' : 'px-1.5'
|
||||
}
|
||||
asChild
|
||||
>
|
||||
<Link href={downloadUrl}>{t('download')}</Link>
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
)}
|
||||
|
||||
{application.scheme && (
|
||||
<CopyToClipboard
|
||||
text={getAppSubLink(url, application.scheme)}
|
||||
onCopy={handleCopy}
|
||||
>
|
||||
<Button
|
||||
size='sm'
|
||||
className={downloadUrl ? 'rounded-l-none p-2' : 'p-2'}
|
||||
>
|
||||
{t('import')}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface GlobalStore {
|
||||
setUser: (user?: API.User) => void;
|
||||
getUserInfo: () => Promise<void>;
|
||||
getUserSubscribe: (uuid: string, type?: string) => string[];
|
||||
getAppSubLink: (type: string, url: string) => string;
|
||||
getAppSubLink: (url: string, schema?: string) => string;
|
||||
}
|
||||
|
||||
export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
||||
@@ -107,45 +107,48 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
||||
}
|
||||
});
|
||||
},
|
||||
getAppSubLink: (type: string, url: string) => {
|
||||
getAppSubLink: (url: string, schema?: string) => {
|
||||
const name = get().common?.site?.site_name || '';
|
||||
switch (type) {
|
||||
case 'Clash':
|
||||
return `clash://install-config?url=${url}&name=${name}`;
|
||||
case 'Hiddify':
|
||||
return `hiddify://import/${url}#${name}`;
|
||||
case 'Loon':
|
||||
return `loon://import?sub=${encodeURIComponent(url)}`;
|
||||
case 'NekoBox':
|
||||
return `sn://subscription?url=${url}&name=${name}`;
|
||||
case 'NekoRay':
|
||||
return `sn://subscription?url=${url}&name=${name}`;
|
||||
// case 'Netch':
|
||||
// return ``;
|
||||
case 'Quantumult X':
|
||||
return `quantumult-x://add-resource?remote-resource=${encodeURIComponent(
|
||||
JSON.stringify({
|
||||
server_remote: [`${url}, tag=${name}`],
|
||||
}),
|
||||
)}`;
|
||||
case 'Shadowrocket':
|
||||
return `shadowrocket://add/sub://${window.btoa(url)}?remark=${encodeURIComponent(name)}`;
|
||||
case 'Singbox':
|
||||
return `sing-box://import-remote-profile?url=${encodeURIComponent(url)}#${name}`;
|
||||
case 'Surfboard':
|
||||
return `surfboard:///install-config?url=${encodeURIComponent(url)}`;
|
||||
case 'Surge':
|
||||
return `surge:///install-config?url=${encodeURIComponent(url)}`;
|
||||
case 'V2box':
|
||||
return `v2box://install-sub?url=${encodeURIComponent(url)}&name=${name}`;
|
||||
// case 'V2rayN':
|
||||
// return `v2rayn://install-sub?url=${encodeURIComponent(url)}&name=${name}`;
|
||||
case 'V2rayNg':
|
||||
return `v2rayng://install-sub?url=${encodeURIComponent(url)}#${name}`;
|
||||
case 'Stash':
|
||||
return `stash://install-config?url=${encodeURIComponent(url)}&name=${name}`;
|
||||
default:
|
||||
return '';
|
||||
|
||||
if (!schema) return url;
|
||||
try {
|
||||
let result = schema;
|
||||
|
||||
result = result.replace(/\${url}/g, url);
|
||||
result = result.replace(/\${name}/g, name);
|
||||
|
||||
result = result.replace(/\${encodeURIComponent\(([^)]+)\)}/g, (match, expr) => {
|
||||
if (expr === 'url') return encodeURIComponent(url);
|
||||
if (expr === 'name') return encodeURIComponent(name);
|
||||
return match;
|
||||
});
|
||||
|
||||
result = result.replace(/\${window\.btoa\(([^)]+)\)}/g, (match, expr) => {
|
||||
const btoa = typeof window !== 'undefined' ? window.btoa : (str: string) => str;
|
||||
if (expr === 'url') return btoa(url);
|
||||
if (expr === 'name') return btoa(name);
|
||||
return match;
|
||||
});
|
||||
|
||||
result = result.replace(/\${JSON\.stringify\(([^}]+)\)}/g, (match, expr) => {
|
||||
try {
|
||||
const processedExpr = expr.replace(/url/g, `"${url}"`).replace(/name/g, `"${name}"`);
|
||||
|
||||
if (processedExpr.includes('server_remote')) {
|
||||
const serverRemoteValue = `${url}, tag=${name}`;
|
||||
return JSON.stringify({ server_remote: [serverRemoteValue] });
|
||||
}
|
||||
|
||||
const result = eval(`(${processedExpr})`);
|
||||
return JSON.stringify(result);
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return url;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-ignore
|
||||
/* eslint-disable */
|
||||
|
||||
import request from '@/utils/request';
|
||||
|
||||
/** Get Ads GET /v1/common/ads */
|
||||
@@ -43,6 +43,17 @@ export async function checkVerificationCode(
|
||||
);
|
||||
}
|
||||
|
||||
/** Get Client GET /v1/common/client */
|
||||
export async function getClient(options?: { [key: string]: any }) {
|
||||
return request<API.Response & { data?: API.GetSubscribeApplicationListResponse }>(
|
||||
'/v1/common/client',
|
||||
{
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Get verification code POST /v1/common/send_code */
|
||||
export async function sendEmailCode(body: API.SendCodeRequest, options?: { [key: string]: any }) {
|
||||
return request<API.Response & { data?: API.SendCodeResponse }>('/v1/common/send_code', {
|
||||
|
||||
+30
@@ -213,6 +213,15 @@ declare namespace API {
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type DownloadLink = {
|
||||
ios?: string;
|
||||
android?: string;
|
||||
windows?: string;
|
||||
mac?: string;
|
||||
linux?: string;
|
||||
harmony?: string;
|
||||
};
|
||||
|
||||
type EmailAuthticateConfig = {
|
||||
enable: boolean;
|
||||
enable_verify: boolean;
|
||||
@@ -284,6 +293,11 @@ declare namespace API {
|
||||
protocol: string[];
|
||||
};
|
||||
|
||||
type GetSubscribeApplicationListResponse = {
|
||||
total: number;
|
||||
list: SubscribeApplication[];
|
||||
};
|
||||
|
||||
type GetTosResponse = {
|
||||
tos_content: string;
|
||||
};
|
||||
@@ -765,6 +779,22 @@ declare namespace API {
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type SubscribeApplication = {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
scheme?: string;
|
||||
user_agent: string;
|
||||
is_default: boolean;
|
||||
proxy_template: string;
|
||||
template: string;
|
||||
output_format: string;
|
||||
download_link?: DownloadLink;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type SubscribeConfig = {
|
||||
single_model: boolean;
|
||||
subscribe_path: string;
|
||||
|
||||
Vendored
+30
@@ -214,6 +214,15 @@ declare namespace API {
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type DownloadLink = {
|
||||
ios?: string;
|
||||
android?: string;
|
||||
windows?: string;
|
||||
mac?: string;
|
||||
linux?: string;
|
||||
harmony?: string;
|
||||
};
|
||||
|
||||
type EmailAuthticateConfig = {
|
||||
enable: boolean;
|
||||
enable_verify: boolean;
|
||||
@@ -266,6 +275,11 @@ declare namespace API {
|
||||
methods: UserAuthMethod[];
|
||||
};
|
||||
|
||||
type GetSubscribeApplicationListResponse = {
|
||||
total: number;
|
||||
list: SubscribeApplication[];
|
||||
};
|
||||
|
||||
type GetSubscribeLogParams = {
|
||||
page: number;
|
||||
size: number;
|
||||
@@ -863,6 +877,22 @@ declare namespace API {
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type SubscribeApplication = {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
scheme?: string;
|
||||
user_agent: string;
|
||||
is_default: boolean;
|
||||
proxy_template: string;
|
||||
template: string;
|
||||
output_format: string;
|
||||
download_link?: DownloadLink;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type SubscribeConfig = {
|
||||
single_model: boolean;
|
||||
subscribe_path: string;
|
||||
|
||||
Reference in New Issue
Block a user