Skip to content

Commit 38246fd

Browse files
committed
feat(ui): replace emoji icons with official client icons for subscription links
1 parent e6009c0 commit 38246fd

File tree

3 files changed

+149
-64
lines changed

3 files changed

+149
-64
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { type SVGProps } from 'react'
2+
3+
export function WireguardIcon({ className, size = 16, ...props }: SVGProps<SVGSVGElement> & { size?: number }) {
4+
return (
5+
<svg
6+
xmlns="http://www.w3.org/2000/svg"
7+
viewBox="0 0 32 32"
8+
width={size}
9+
height={size}
10+
fill="currentColor"
11+
className={className}
12+
{...props}
13+
>
14+
<path d="M31.973 15.527S32.708 0 15.645 0C.557 0 .082 14.896.082 14.896S-2.137 32 15.994 32c17.391 0 15.979-16.473 15.979-16.473m-21.098-5.428c3.199-1.959 7.292-.76 8.823 2.188c.292.557.329 1.416.147 2.005c-.641 2.02-2.136 3.151-4.199 3.635c.609-.52 1.095-1.109 1.245-1.921a2.84 2.84 0 0 0-.484-2.24a2.86 2.86 0 0 0-3.287-1c-1.265.485-1.959 1.641-1.833 3.063c.115 1.317 1.115 2.177 2.989 2.5a18 18 0 0 0-.703.375a6.8 6.8 0 0 0-2.193 1.907c-.192.255-.323.276-.615.099c-3.765-2.301-4.011-8.088.104-10.609zM8.052 24.344c-.604.156-1.192.38-1.812.583c.301-2.041 2.697-3.921 4.719-3.708a5.2 5.2 0 0 0-.985 2.76c-.671.125-1.307.209-1.921.365zM20.937 4.401c.599.02 1.199.011 1.797.025c.151.011.296.032.443.063a4.6 4.6 0 0 1-.453.579c-.213.197-.453.396-.76.093c-.079-.073-.251-.057-.38-.063c-.595-.005-1.193-.025-1.787 0a11 11 0 0 0-1.537.156c-.099.016-.239.333-.197.448c.104.276.255.584.479.761c.828.651 1.704 1.233 2.537 1.88c.801.629 1.552 1.323 2.009 2.271c.595 1.235.615 2.531.36 3.833c-.432 2.172-1.531 3.973-3.312 5.281c-.72.525-1.609.828-2.428 1.203c-.724.333-1.468.62-2.192.953c-1.308.593-2.043 2.011-1.828 3.484c.197 1.355 1.391 2.485 2.744 2.719c1.636.276 3.317-.781 3.713-2.443c.448-1.859-.561-3.525-2.457-4.031l-.339-.088c.505-.224.943-.387 1.348-.609a40 40 0 0 0 2.073-1.235c.203-.131.312-.131.484.02c1.303 1.125 2.079 2.532 2.297 4.251c.36 2.844-.984 5.459-3.525 6.796c-3.933 2.073-8.745-.285-9.615-4.645c-.74-3.729 1.891-7.12 5.061-7.771c1.365-.281 2.615-.849 3.584-1.901c.625-.677.927-1.255 1.031-1.52a4.2 4.2 0 0 0 .292-1.543a3.6 3.6 0 0 0-.317-1.323c-.328-.755-1.599-1.959-1.912-2.208l-2.989-2.339c-.104-.088-.224-.084-.479-.063c-.308.021-1.089.063-1.423-.025c.271-.209 1.011-.505 1.333-.745c-.968-.656-2.072-.421-3.088-.615c.235-.437 1.396-1.109 2.057-1.183a10 10 0 0 0-.183-1.099c-.036-.145-.203-.292-.349-.375c-.348-.208-.724-.375-1.124-.579a2.34 2.34 0 0 1 1.208-.375c.407-.016.812.027 1.208.12c.724.161 1.297.052 1.871-.432c-.453-.183-.901-.349-1.339-.547a12 12 0 0 1-1.256-.657c1.131.156 2.224.584 3.385.428l.032-.157L14.321.57c1.604-.152 3.099-.172 4.511.515c.401.193.812.355 1.197.573c.188.109.313.323.464.489c.12.131.219.308.369.385c.563.303 1.183.313 1.819.297l.015-.213c.641.203 1.355.937 1.355 1.473c-1.031 0-2.068-.005-3.099.005c-.109 0-.219.084-.328.125c.104.063.203.172.312.177zm-1.333-1.532a.157.157 0 0 0-.02.256a.237.237 0 0 0 .328.083c.099-.047.197-.104.317-.167c-.099-.077-.177-.151-.256-.213c-.14-.12-.255-.041-.369.041" />
15+
</svg>
16+
)
17+
}
18+
19+
export function XrayIcon({ className, size = 16, ...props }: SVGProps<SVGSVGElement> & { size?: number }) {
20+
return (
21+
<svg
22+
xmlns="http://www.w3.org/2000/svg"
23+
viewBox="0 0 35 35"
24+
width={size}
25+
height={size}
26+
fill="currentColor"
27+
className={className}
28+
{...props}
29+
>
30+
<path d="M16.6961 15.2606L16.5825 3.49701C16.5718 2.38439 15.025 2.11843 14.6433 3.16356L11.7279 11.1447C11.6384 11.3898 11.4566 11.5902 11.2213 11.7031L5.66765 14.3687C4.70841 14.8291 5.03635 16.2703 6.10036 16.2703H15.6962C16.2522 16.2703 16.7015 15.8166 16.6961 15.2606Z" />
31+
<path d="M18.6471 15.2703V5.88936C18.6471 4.84679 20.0428 4.49998 20.5308 5.4213L23.5833 11.1845C23.7 11.4049 23.8948 11.5737 24.1296 11.6578L31.5829 14.3289C32.6388 14.7073 32.3671 16.2703 31.2455 16.2703H19.6471C19.0948 16.2703 18.6471 15.8226 18.6471 15.2703Z" />
32+
<path d="M18.6471 31.4643V19.3784C18.6471 18.8261 19.0948 18.3784 19.6471 18.3784H29.2853C30.3376 18.3784 30.676 19.7947 29.7374 20.2704L24.1129 23.1208C23.889 23.2343 23.716 23.4278 23.6281 23.663L20.5839 31.8141C20.1941 32.8578 18.6471 32.5783 18.6471 31.4643Z" />
33+
<path d="M16.7059 28.9873V19.3784C16.7059 18.8261 16.2582 18.3784 15.7059 18.3784H3.83963C2.71522 18.3784 2.44656 19.9473 3.50691 20.3214L11.5457 23.1578C11.7987 23.247 12.0052 23.4342 12.1188 23.6772L14.8 29.4109C15.2531 30.3797 16.7059 30.0568 16.7059 28.9873Z" />
34+
</svg>
35+
)
36+
}
37+
38+
export function SingboxIcon({ className, size = 16, ...props }: SVGProps<SVGSVGElement> & { size?: number }) {
39+
return (
40+
<svg
41+
xmlns="http://www.w3.org/2000/svg"
42+
viewBox="0 0 35 35"
43+
width={size}
44+
height={size}
45+
fill="none"
46+
stroke="currentColor"
47+
strokeWidth="3.5"
48+
strokeLinecap="round"
49+
strokeLinejoin="round"
50+
className={className}
51+
{...props}
52+
>
53+
<path d="M17.5 31.5C16.823 31.5 16.1588 31.3169 15.5786 30.9702L6.82402 25.7514C6.26677 25.42 5.80392 24.9492 5.48419 24.3877C5.17207 23.8277 5.00459 23.198 5.00003 22.5578V11.8645C4.99755 11.218 5.16443 10.582 5.48425 10.0191C5.80407 9.45625 6.26581 8.98584 6.82402 8.65419L15.5786 4.01512C16.162 3.67773 16.825 3.5 17.5 3.5C18.175 3.5 18.838 3.67773 19.4214 4.01512L28.176 8.65419C28.7344 8.98597 29.1963 9.45662 29.5161 10.0198C29.836 10.583 30.0027 11.2193 30 11.866V22.5563C29.9954 23.198 29.8279 23.8277 29.5158 24.3877C29.1961 24.9492 28.7332 25.42 28.176 25.7514L19.4214 30.9702C18.8412 31.3169 18.177 31.5 17.5 31.5ZM17.5 31.5V17.2119M29.4991 10.0497L17.5 17.2119M17.5 17.2119L5.50093 10.0499M24.1687 18.9134V13.2299L11.5104 6.18126" />
54+
</svg>
55+
)
56+
}
57+
58+
export function MihomoIcon({ className, size = 16, ...props }: SVGProps<SVGSVGElement> & { size?: number }) {
59+
return (
60+
<svg
61+
xmlns="http://www.w3.org/2000/svg"
62+
viewBox="0 0 35 35"
63+
width={size}
64+
height={size}
65+
fill="currentColor"
66+
className={className}
67+
{...props}
68+
>
69+
<path
70+
clipRule="evenodd"
71+
d="M3.858 3.652c-.654.074-1.225.156-1.27.183-.126.078-.112 19.612.014 19.716.053.043.88.25 1.841.46c1.427.313 1.879.383 2.472.383.604 0 .747-.023.85-.136.112-.123.126-.652.144-5.418l.02-5.282.214-.181c.265-.227.574-.232.854-.014.118.092 1.22 1.074 2.45 2.181c1.229 1.108 2.312 2.033 2.407 2.057.095.023.49-.027.878-.113c1.142-.253 1.633-.308 2.716-.308c1.012 0 2.003.117 2.93.348c.263.065.567.097.675.07c.108-.027.612-.424 1.12-.884l2.442-2.203c.459-.414.85-.77.872-.79c.582-.579.935-.662 1.295-.306l.221.22V24.16l.169.117c.306.212 1.41.154 2.734-.143a98.47 98.47 0 0 1 1.762-.38c.35-.07.646-.173.701-.244.143-.184.138-19.396-.004-19.59-.082-.11-.352-.16-1.498-.28-.77-.08-1.573-.136-1.788-.124l-.389.022-4.038 3.645c-5.574 5.032-5.244 4.746-5.422 4.684-.084-.029-.977-.055-1.986-.058c-1.743-.005-1.842-.013-2.01-.161-.414-.367-2.99-2.686-4.844-4.36c-1.103-.997-2.5-2.257-3.105-2.8l-1.1-.988-.568.01c-.313.005-1.104.069-1.759.142Z"
72+
fillRule="evenodd"
73+
/>
74+
<path d="M17.89 26.569a.452.452 0 0 1-.78 0l-.651-1.12a.448.448 0 0 1 .39-.672h1.301c.347 0 .564.373.39.672l-.65 1.12ZM2.5 26.121c0-.289.235-.523.526-.523h7.212c.29 0 .526.234.526.523a.524.524 0 0 1-.526.523H3.026a.524.524 0 0 1-.526-.523ZM2.901 31.152a.522.522 0 0 1 .32-.667l6.796-2.397a.522.522 0 1 1 .352.985L3.573 31.47a.527.527 0 0 1-.672-.318ZM32.5 26.121a.524.524 0 0 0-.526-.523h-7.212a.524.524 0 0 0-.526.523c0 .289.236.523.526.523h7.212c.29 0 .526-.234.526-.523ZM32.099 31.152a.522.522 0 0 0-.32-.667l-6.796-2.397a.522.522 0 1 0-.352.985l6.796 2.397a.527.527 0 0 0 .672-.318Z" />
75+
</svg>
76+
)
77+
}

dashboard/src/components/users/action-buttons.tsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import useDirDetection from '@/hooks/use-dir-detection'
55
import { type UseEditFormValues } from '@/components/forms/user-form'
66
import { useActiveNextPlan, useGetCurrentAdmin, useRemoveUser, useResetUserDataUsage, useRevokeUserSubscription, UserResponse, UsersResponse } from '@/service/api'
77
import { useQueryClient } from '@tanstack/react-query'
8-
import { Check, Copy, Cpu, EllipsisVertical, Link2Off, ListStart, Network, Pencil, PieChart, QrCode, RefreshCcw, Trash2, User, Users } from 'lucide-react'
8+
import { Cat, Check, Copy, Cpu, EllipsisVertical, GlobeLock, Link2Off, ListStart, ListTree, Network, Pencil, PieChart, QrCode, RefreshCcw, Trash2, User, Users } from 'lucide-react'
9+
import { WireguardIcon, XrayIcon, SingboxIcon, MihomoIcon } from '@/components/icons/format-icons'
10+
import { Code } from 'lucide-react'
911
import { FC, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
1012
import { useForm } from 'react-hook-form'
1113
import { useTranslation } from 'react-i18next'
@@ -31,7 +33,7 @@ type ActionButtonsProps = {
3133
export interface SubscribeLink {
3234
protocol: string
3335
link: string
34-
icon: string
36+
icon: React.ComponentType<{ className?: string }>
3537
}
3638

3739
const DOWNLOAD_ONLY_PROTOCOLS = ['clash', 'clash-meta', 'sing-box']
@@ -325,14 +327,14 @@ const ActionButtons: FC<ActionButtonsProps> = ({ user, isModalHost = true, rende
325327
const subURL = user.subscription_url.startsWith('/') ? window.location.origin + user.subscription_url : user.subscription_url
326328

327329
const links = [
328-
{ protocol: 'links', link: `${subURL}/links`, icon: '🔗' },
329-
{ protocol: 'links (base64)', link: `${subURL}/links_base64`, icon: '📝' },
330-
{ protocol: 'xray', link: `${subURL}/xray`, icon: '⚡' },
331-
{ protocol: 'wireguard', link: `${subURL}/wireguard`, icon: '🛜' },
332-
{ protocol: 'clash', link: `${subURL}/clash`, icon: '⚔️' },
333-
{ protocol: 'clash-meta', link: `${subURL}/clash_meta`, icon: '🛡️' },
334-
{ protocol: 'outline', link: `${subURL}/outline`, icon: '🔒' },
335-
{ protocol: 'sing-box', link: `${subURL}/sing_box`, icon: '📦' },
330+
{ protocol: 'links', link: `${subURL}/links`, icon: ListTree },
331+
{ protocol: 'links (base64)', link: `${subURL}/links_base64`, icon: Code },
332+
{ protocol: 'xray', link: `${subURL}/xray`, icon: XrayIcon },
333+
{ protocol: 'wireguard', link: `${subURL}/wireguard`, icon: WireguardIcon },
334+
{ protocol: 'clash', link: `${subURL}/clash`, icon: Cat },
335+
{ protocol: 'clash-meta', link: `${subURL}/clash_meta`, icon: MihomoIcon },
336+
{ protocol: 'outline', link: `${subURL}/outline`, icon: GlobeLock },
337+
{ protocol: 'sing-box', link: `${subURL}/sing_box`, icon: SingboxIcon },
336338
]
337339
setSubscribeLinks(links)
338340
}
@@ -562,13 +564,13 @@ const ActionButtons: FC<ActionButtonsProps> = ({ user, isModalHost = true, rende
562564
})
563565
}
564566

565-
const handleLinksCopy = async (link: string, type: string, icon: string) => {
567+
const handleLinksCopy = async (link: string, type: string) => {
566568
try {
567569
const cachedContent = configContentCacheRef.current[link]
568570
if (cachedContent !== undefined) {
569571
const copiedSuccessfully = await copy(cachedContent)
570572
if (copiedSuccessfully) {
571-
toast.success(`${icon} ${type} ${t('usersTable.copied', { defaultValue: 'Copied to clipboard' })}`)
573+
toast.success(`${type} ${t('usersTable.copied', { defaultValue: 'Copied to clipboard' })}`)
572574
} else {
573575
toast.error(t('copyFailed', { defaultValue: 'Failed to copy content' }))
574576
}
@@ -586,7 +588,7 @@ const ActionButtons: FC<ActionButtonsProps> = ({ user, isModalHost = true, rende
586588
const content = await fetchAndCacheContent(link)
587589
const copiedSuccessfully = await copy(content)
588590
if (copiedSuccessfully) {
589-
toast.success(`${icon} ${type} ${t('usersTable.copied', { defaultValue: 'Copied to clipboard' })}`)
591+
toast.success(`${type} ${t('usersTable.copied', { defaultValue: 'Copied to clipboard' })}`)
590592
} else {
591593
toast.error(t('copyFailed', { defaultValue: 'Failed to copy content' }))
592594
}
@@ -624,15 +626,15 @@ const ActionButtons: FC<ActionButtonsProps> = ({ user, isModalHost = true, rende
624626
}
625627
}
626628

627-
const handleCopyOrDownload = (link: string, type: string, icon: string) => {
628-
if (type === 'wireguard') {
629-
window.open(link, '_blank')
630-
} else if (DOWNLOAD_ONLY_PROTOCOLS.includes(type)) {
631-
handleConfigDownload(link, type)
632-
} else {
633-
handleLinksCopy(link, type, icon)
629+
const handleCopyOrDownload = (link: string, type: string) => {
630+
if (type === 'wireguard') {
631+
window.open(link, '_blank')
632+
} else if (DOWNLOAD_ONLY_PROTOCOLS.includes(type)) {
633+
handleConfigDownload(link, type)
634+
} else {
635+
handleLinksCopy(link, type)
636+
}
634637
}
635-
}
636638

637639
return (
638640
<div onClick={renderActions ? (e => e.stopPropagation()) : undefined}>
@@ -665,8 +667,8 @@ const handleCopyOrDownload = (link: string, type: string, icon: string) => {
665667
</DropdownMenuTrigger>
666668
<DropdownMenuContent>
667669
{subscribeLinks.map((item, index) => (
668-
<DropdownMenuItem dir='ltr' key={index} onClick={() => handleCopyOrDownload(item.link, item.protocol, item.icon)}>
669-
<span className="mr-2">{item.icon}</span>
670+
<DropdownMenuItem dir='ltr' key={index} onClick={() => handleCopyOrDownload(item.link, item.protocol)}>
671+
<item.icon className="mr-2 h-4 w-4" />
670672
{item.protocol}
671673
</DropdownMenuItem>
672674
))}

0 commit comments

Comments
 (0)