From 123fac222bc28e3bad6f50339b47833bb2a2c451 Mon Sep 17 00:00:00 2001 From: w3struk Date: Tue, 21 Jul 2026 18:52:33 +0500 Subject: [PATCH] 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 --- .../src/pages/clients/ClientInfoModal.tsx | 35 ++++++++++- frontend/src/pages/sub/SubPage.tsx | 21 +++++++ internal/sub/controller.go | 34 +++++++--- internal/sub/controller_test.go | 62 +++++++++++++++++++ internal/sub/dist.go | 8 +-- 5 files changed, 146 insertions(+), 14 deletions(-) diff --git a/frontend/src/pages/clients/ClientInfoModal.tsx b/frontend/src/pages/clients/ClientInfoModal.tsx index 13441d693..d09c9990c 100644 --- a/frontend/src/pages/clients/ClientInfoModal.tsx +++ b/frontend/src/pages/clients/ClientInfoModal.tsx @@ -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(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({