mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-23 04:56:07 +00:00
feat(sub): add raw subscription download actions (#6017)
* feat(sub): add raw subscription downloads * fix(sub): address review feedback * feat(sub): add download buttons to client subscriptions * fix(sub): fetch subscription before download --------- Co-authored-by: w3struk <w3struk@gmail.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, Modal, Popover, Tag, Tooltip, message } from 'antd';
|
||||
import { CopyOutlined, EyeOutlined, QrcodeOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { CopyOutlined, DownloadOutlined, EyeOutlined, QrcodeOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
|
||||
import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils';
|
||||
import { ClipboardManager, FileManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils';
|
||||
import { formatInboundLabel } from '@/lib/inbounds/label';
|
||||
import { normalizeClientIps, type ClientIpInfo } from '@/lib/clients/ip-log';
|
||||
import { useDatepicker } from '@/hooks/useDatepicker';
|
||||
@@ -64,6 +64,12 @@ const DEFAULT_SUB: SubSettings = {
|
||||
publicHost: '',
|
||||
};
|
||||
|
||||
const SUBSCRIPTION_DOWNLOAD_NAMES = {
|
||||
standard: 'subscription-standard.txt',
|
||||
json: 'subscription-json.json',
|
||||
clash: 'subscription-clash.yaml',
|
||||
} as const;
|
||||
|
||||
export default function ClientInfoModal({
|
||||
open,
|
||||
client,
|
||||
@@ -89,6 +95,7 @@ export default function ClientInfoModal({
|
||||
const [ipsLoading, setIpsLoading] = useState(false);
|
||||
const [ipsClearing, setIpsClearing] = useState(false);
|
||||
const [ipsModalOpen, setIpsModalOpen] = useState(false);
|
||||
const [downloadingFormat, setDownloadingFormat] = useState<keyof typeof SUBSCRIPTION_DOWNLOAD_NAMES | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
@@ -148,6 +155,21 @@ export default function ClientInfoModal({
|
||||
if (ok) messageApi.success(t('copied'));
|
||||
}
|
||||
|
||||
async function downloadSubscription(url: string, format: keyof typeof SUBSCRIPTION_DOWNLOAD_NAMES) {
|
||||
if (!url || downloadingFormat) return;
|
||||
setDownloadingFormat(format);
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Subscription download failed');
|
||||
const content = await response.text();
|
||||
FileManager.downloadTextFile(content, SUBSCRIPTION_DOWNLOAD_NAMES[format]);
|
||||
} catch (_) {
|
||||
messageApi.error(t('somethingWentWrong'));
|
||||
} finally {
|
||||
setDownloadingFormat(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadIps() {
|
||||
if (!client?.email) return;
|
||||
setIpsLoading(true);
|
||||
@@ -383,6 +405,9 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button size="small" icon={<CopyOutlined />} aria-label={t('copy')} onClick={() => copyValue(subLink)} />
|
||||
</Tooltip>
|
||||
<Tooltip title={t('download')}>
|
||||
<Button size="small" icon={<DownloadOutlined />} aria-label={t('download')} loading={downloadingFormat === 'standard'} disabled={downloadingFormat !== null} onClick={() => void downloadSubscription(subLink, 'standard')} />
|
||||
</Tooltip>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="left"
|
||||
@@ -411,6 +436,9 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button size="small" icon={<CopyOutlined />} aria-label={t('copy')} onClick={() => copyValue(subJsonLink)} />
|
||||
</Tooltip>
|
||||
<Tooltip title={t('download')}>
|
||||
<Button size="small" icon={<DownloadOutlined />} aria-label={t('download')} loading={downloadingFormat === 'json'} disabled={downloadingFormat !== null} onClick={() => void downloadSubscription(subJsonLink, 'json')} />
|
||||
</Tooltip>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="left"
|
||||
@@ -442,6 +470,9 @@ export default function ClientInfoModal({
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button size="small" icon={<CopyOutlined />} aria-label={t('copy')} onClick={() => copyValue(subClashLink)} />
|
||||
</Tooltip>
|
||||
<Tooltip title={t('download')}>
|
||||
<Button size="small" icon={<DownloadOutlined />} aria-label={t('download')} loading={downloadingFormat === 'clash'} disabled={downloadingFormat !== null} onClick={() => void downloadSubscription(subClashLink, 'clash')} />
|
||||
</Tooltip>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="left"
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
AppleOutlined,
|
||||
CopyOutlined,
|
||||
DownOutlined,
|
||||
DownloadOutlined,
|
||||
MoonFilled,
|
||||
MoonOutlined,
|
||||
QrcodeOutlined,
|
||||
@@ -64,6 +65,8 @@ const subEmail = [...new Set(linkEmails.filter(Boolean))].join(', ');
|
||||
const datepicker = subData.datepicker || 'gregorian';
|
||||
const announce = subData.announce || '';
|
||||
|
||||
const appendRawView = (url: string) => `${url}${url.includes('?') ? '&' : '?'}view=raw`;
|
||||
|
||||
const isUnlimited = totalByte <= 0 && expireMs === 0;
|
||||
const isActive = (() => {
|
||||
if (!enabled) return false;
|
||||
@@ -354,6 +357,15 @@ export default function SubPage() {
|
||||
{sId}
|
||||
</a>
|
||||
<div className="sub-link-actions">
|
||||
<Button
|
||||
size="small"
|
||||
href={appendRawView(subJsonUrl)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
icon={<DownloadOutlined />}
|
||||
aria-label={t('download')}
|
||||
title={t('download')}
|
||||
/>
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => copy(subJsonUrl)} aria-label={t('copy')} title={t('copy')} />
|
||||
<Popover
|
||||
trigger="click"
|
||||
@@ -386,6 +398,15 @@ export default function SubPage() {
|
||||
{sId}
|
||||
</a>
|
||||
<div className="sub-link-actions">
|
||||
<Button
|
||||
size="small"
|
||||
href={appendRawView(subClashUrl)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
icon={<DownloadOutlined />}
|
||||
aria-label={t('download')}
|
||||
title={t('download')}
|
||||
/>
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => copy(subClashUrl)} aria-label={t('copy')} title={t('copy')} />
|
||||
<Popover
|
||||
trigger="click"
|
||||
|
||||
Reference in New Issue
Block a user