Skip to content

Commit 453f7b5

Browse files
committed
feat: add variables popover for subscription settings
1 parent 4625e6a commit 453f7b5

File tree

7 files changed

+137
-96
lines changed

7 files changed

+137
-96
lines changed

dashboard/public/statics/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,8 @@
942942
"usage_percentage": "Data usage percentage",
943943
"protocol": "Proxy protocol (e.g. VMess)",
944944
"transport": "Proxy transport method (e.g. ws)",
945+
"profile_title": "Profile title",
946+
"url": "Subscription URL",
945947
"admin_username": "Admin username"
946948
},
947949
"advancedOptions": "Advanced Options",

dashboard/public/statics/locales/fa.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,8 @@
821821
"usage_percentage": "درصد استفاده از داده",
822822
"protocol": "پروتکل پروکسی (مثال: VMess)",
823823
"transport": "روش انتقال پروکسی (مثال: ws)",
824+
"profile_title": "عنوان پروفایل",
825+
"url": "آدرس اشتراک",
824826
"admin_username": "نام کاربری ادمین"
825827
},
826828
"advancedOptions": "تنظیمات پیشرفته",

dashboard/public/statics/locales/ru.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,8 @@
10991099
"usage_percentage": "Процент использования данных",
11001100
"protocol": "Протокол прокси (например, VMess)",
11011101
"transport": "Метод транспорта прокси (например, ws)",
1102+
"profile_title": "Название профиля",
1103+
"url": "URL подписки",
11021104
"admin_username": "Имя пользователя администратора"
11031105
},
11041106
"httpVersions": {

dashboard/public/statics/locales/zh.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,8 @@
915915
"usage_percentage": "数据使用百分比",
916916
"protocol": "代理协议 (例如:VMess)",
917917
"transport": "代理传输方法 (例如:ws)",
918+
"profile_title": "配置文件标题",
919+
"url": "订阅链接",
918920
"admin_username": "管理员用户名"
919921
},
920922
"advancedOptions": "高级选项",

dashboard/src/components/apps/sortable-application.tsx

Lines changed: 61 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Switch } from '@/components/ui/switch'
1111
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
1212
import { GripVertical, Trash2, Plus, ChevronDown, ChevronRight, Apple, Tv, Monitor, Laptop, Smartphone, Star } from 'lucide-react'
1313
import useDirDetection from '@/hooks/use-dir-detection'
14+
import { VariablesPopover } from '@/components/ui/variables-popover'
1415

1516
const platformOptions = [
1617
{ value: 'android', label: 'settings.subscriptions.applications.platforms.android' },
@@ -89,25 +90,25 @@ export function SortableApplication({ index, onRemove, form, id }: SortableAppli
8990
<div ref={setNodeRef} style={style} className="cursor-default">
9091
<div className="group relative rounded-md border bg-card transition-colors hover:bg-accent/20">
9192
{/* Header with drag handle, expand/collapse, and delete button */}
92-
<div className="flex items-center gap-3 p-4">
93-
<button type="button" style={{ cursor: cursor }} className="touch-none opacity-50 transition-opacity group-hover:opacity-100" {...attributes} {...listeners}>
94-
<GripVertical className="h-5 w-5" />
93+
<div className="flex items-center gap-2 p-3 sm:gap-3 sm:p-4">
94+
<button type="button" style={{ cursor: cursor }} className="touch-none shrink-0 opacity-50 transition-opacity group-hover:opacity-100" {...attributes} {...listeners}>
95+
<GripVertical className="h-4 w-4 sm:h-5 sm:w-5" />
9596
<span className="sr-only">Drag to reorder</span>
9697
</button>
9798

98-
<button type="button" onClick={() => setIsExpanded(!isExpanded)} className={'flex flex-1 flex-wrap items-center gap-2 hover:text-foreground sm:flex-nowrap'}>
99-
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
99+
<button type="button" onClick={() => setIsExpanded(!isExpanded)} className={'flex flex-1 min-w-0 flex-wrap items-center gap-1.5 hover:text-foreground sm:gap-2 sm:flex-nowrap'}>
100+
{isExpanded ? <ChevronDown className="h-4 w-4 shrink-0" /> : <ChevronRight className="h-4 w-4 shrink-0" />}
100101
{/* Icon preview with graceful fallback */}
101102
{(() => {
102103
const iconUrl = form.watch(`applications.${index}.icon_url`)
103104
const name = (form.watch(`applications.${index}.name`) || '').trim()
104105
const initial = name ? name.charAt(0).toUpperCase() : ''
105106
const platform = form.watch(`applications.${index}.platform`)
106107
if (iconUrl && !iconBroken) {
107-
return <img src={iconUrl} alt={name || 'icon'} className="h-5 w-5 rounded-sm object-cover" onError={() => setIconBroken(true)} onClick={e => e.stopPropagation()} />
108+
return <img src={iconUrl} alt={name || 'icon'} className="h-4 w-4 shrink-0 rounded-sm object-cover sm:h-5 sm:w-5" onError={() => setIconBroken(true)} onClick={e => e.stopPropagation()} />
108109
}
109110
return (
110-
<span aria-label="app-icon-fallback" className="inline-flex h-5 w-5 items-center justify-center overflow-hidden rounded-sm bg-muted text-muted-foreground/90">
111+
<span aria-label="app-icon-fallback" className="inline-flex h-4 w-4 shrink-0 items-center justify-center overflow-hidden rounded-sm bg-muted text-muted-foreground/90 sm:h-5 sm:w-5">
111112
{initial ? (
112113
<span className="text-[10px] font-medium leading-none">{initial}</span>
113114
) : (
@@ -122,13 +123,13 @@ export function SortableApplication({ index, onRemove, form, id }: SortableAppli
122123
<FormField
123124
control={form.control}
124125
name={`applications.${index}.platform`}
125-
render={({ field }) => <span className="text-xs text-muted-foreground">{t(platformOptions.find(o => o.value === field.value)?.label || '')}</span>}
126+
render={({ field }) => <span className="hidden text-xs text-muted-foreground sm:inline">{t(platformOptions.find(o => o.value === field.value)?.label || '')}</span>}
126127
/>
127128
<FormField
128129
control={form.control}
129130
name={`applications.${index}.platform`}
130131
render={({ field }) => (
131-
<span className="text-muted-foreground/80">
132+
<span className="text-muted-foreground/80 shrink-0">
132133
<PlatformIcon platform={field.value} />
133134
</span>
134135
)}
@@ -137,11 +138,11 @@ export function SortableApplication({ index, onRemove, form, id }: SortableAppli
137138
control={form.control}
138139
name={`applications.${index}.name`}
139140
render={({ field }) => (
140-
<h4 className="flex min-w-0 items-center gap-1.5 truncate text-sm font-medium">
141+
<h4 className="flex min-w-0 items-center gap-1.5 truncate text-xs font-medium sm:text-sm">
141142
{field.value || t('settings.subscriptions.applications.application', { defaultValue: 'Application' })}
142143
{form.watch(`applications.${index}.recommended`) ? (
143-
<span title={t('settings.subscriptions.applications.recommended')} className="inline-flex items-center text-amber-500/90">
144-
<Star className="h-3.5 w-3.5 fill-amber-500/30" />
144+
<span title={t('settings.subscriptions.applications.recommended')} className="inline-flex shrink-0 items-center text-amber-500/90">
145+
<Star className="h-3 w-3 fill-amber-500/30 sm:h-3.5 sm:w-3.5" />
145146
</span>
146147
) : null}
147148
</h4>
@@ -158,16 +159,16 @@ export function SortableApplication({ index, onRemove, form, id }: SortableAppli
158159
e.stopPropagation()
159160
onRemove(index)
160161
}}
161-
className="h-8 w-8 shrink-0 p-0 text-destructive opacity-70 transition-opacity hover:bg-destructive/10 hover:text-destructive hover:opacity-100"
162+
className="h-7 w-7 shrink-0 p-0 text-destructive opacity-70 transition-opacity hover:bg-destructive/10 hover:text-destructive hover:opacity-100 sm:h-8 sm:w-8"
162163
>
163-
<Trash2 className="h-4 w-4" />
164+
<Trash2 className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
164165
</Button>
165166
</div>
166167

167168
{/* Collapsible content */}
168169
{isExpanded && (
169-
<div className="border-t bg-muted/20 p-4">
170-
<div className="space-y-4">
170+
<div className="border-t bg-muted/20 p-3 sm:p-4">
171+
<div className="space-y-3 sm:space-y-4">
171172
{/* Application fields */}
172173
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
173174
<FormField
@@ -241,7 +242,10 @@ export function SortableApplication({ index, onRemove, form, id }: SortableAppli
241242
name={`applications.${index}.import_url`}
242243
render={({ field }) => (
243244
<FormItem className="space-y-1 sm:col-span-2">
244-
<FormLabel className="text-xs text-muted-foreground/80">{t('settings.subscriptions.applications.importUrl')}</FormLabel>
245+
<div className="flex items-center gap-1.5">
246+
<FormLabel className="text-xs text-muted-foreground/80">{t('settings.subscriptions.applications.importUrl')}</FormLabel>
247+
<VariablesPopover includeProfileTitle={true} />
248+
</div>
245249
<FormControl>
246250
<Input placeholder={t('settings.subscriptions.applications.importUrlPlaceholder')} {...field} className="h-8 text-left font-mono text-xs" dir="ltr" />
247251
</FormControl>
@@ -258,9 +262,9 @@ export function SortableApplication({ index, onRemove, form, id }: SortableAppli
258262
<FormItem className="space-y-1 sm:col-span-2">
259263
<FormLabel className="text-xs text-muted-foreground/80">{t('settings.subscriptions.applications.descriptionApp')}</FormLabel>
260264
<FormControl>
261-
<div className="flex gap-2">
265+
<div className="flex flex-col gap-2 sm:flex-row">
262266
<Select value={selectedLanguage} onValueChange={setSelectedLanguage}>
263-
<SelectTrigger className="h-8 w-32 text-xs">
267+
<SelectTrigger className="h-8 w-full text-xs sm:w-32">
264268
<SelectValue />
265269
</SelectTrigger>
266270
<SelectContent className="scrollbar-thin z-[50]">
@@ -316,13 +320,14 @@ export function SortableApplication({ index, onRemove, form, id }: SortableAppli
316320

317321
{/* Download Links */}
318322
<div className="space-y-2">
319-
<div className="flex items-center justify-between">
323+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
320324
<FormLabel className="text-xs font-medium text-muted-foreground/80">
321325
{t('settings.subscriptions.applications.downloadLinks')} ({downloadLinkFields.length})
322326
</FormLabel>
323-
<Button type="button" variant="outline" size="sm" onClick={addDownloadLink} className="h-7 text-xs">
327+
<Button type="button" variant="outline" size="sm" onClick={addDownloadLink} className="h-7 w-full text-xs sm:w-auto">
324328
<Plus className="mr-1 h-3 w-3" />
325-
{t('settings.subscriptions.applications.addDownloadLink')}
329+
<span className="hidden sm:inline">{t('settings.subscriptions.applications.addDownloadLink')}</span>
330+
<span className="sm:hidden">{t('settings.subscriptions.applications.addDownloadLink', { defaultValue: 'Add Link' })}</span>
326331
</Button>
327332
</div>
328333

@@ -342,12 +347,12 @@ export function SortableApplication({ index, onRemove, form, id }: SortableAppli
342347
const linkLang = form.watch(`applications.${index}.download_links.${linkIndex}.language`)
343348
const ltrForThis = isRtl && linkLang !== 'fa'
344349
return (
345-
<div key={linkField.id} className="flex gap-2 rounded-md border bg-muted/20 p-2">
350+
<div key={linkField.id} className="flex flex-col gap-2 rounded-md border bg-muted/20 p-2 sm:flex-row">
346351
<FormField
347352
control={form.control}
348353
name={`applications.${index}.download_links.${linkIndex}.name`}
349354
render={({ field }) => (
350-
<FormItem className="flex-1">
355+
<FormItem className="flex-1 min-w-0">
351356
<FormControl>
352357
<Input
353358
placeholder={t('settings.subscriptions.applications.downloadLinkNamePlaceholder')}
@@ -368,7 +373,7 @@ export function SortableApplication({ index, onRemove, form, id }: SortableAppli
368373
control={form.control}
369374
name={`applications.${index}.download_links.${linkIndex}.url`}
370375
render={({ field }) => (
371-
<FormItem className="flex-1">
376+
<FormItem className="flex-1 min-w-0">
372377
<FormControl>
373378
<Input placeholder={t('settings.subscriptions.applications.downloadLinkUrlPlaceholder')} {...field} className="h-7 text-left font-mono text-xs" dir="ltr" />
374379
</FormControl>
@@ -378,35 +383,37 @@ export function SortableApplication({ index, onRemove, form, id }: SortableAppli
378383
</FormItem>
379384
)}
380385
/>
381-
<FormField
382-
control={form.control}
383-
name={`applications.${index}.download_links.${linkIndex}.language`}
384-
render={({ field }) => (
385-
<FormItem className="w-24">
386-
<Select onValueChange={field.onChange} value={field.value}>
387-
<FormControl>
388-
<SelectTrigger className="h-7 text-xs">
389-
<SelectValue />
390-
</SelectTrigger>
391-
</FormControl>
392-
<SelectContent className="scrollbar-thin z-[50]">
393-
{languageOptions.map(option => (
394-
<SelectItem key={option.value} value={option.value}>
395-
<div className="flex items-center gap-1.5">
396-
<span className="text-xs">{option.icon}</span>
397-
<span className="text-xs">{option.label}</span>
398-
</div>
399-
</SelectItem>
400-
))}
401-
</SelectContent>
402-
</Select>
403-
<FormMessage />
404-
</FormItem>
405-
)}
406-
/>
407-
<Button type="button" variant="ghost" size="icon" onClick={() => removeDownloadLink(linkIndex)} className="h-7 w-7 p-0 text-destructive hover:bg-destructive/10">
408-
<Trash2 className="h-3 w-3" />
409-
</Button>
386+
<div className="flex items-start gap-2 sm:items-center">
387+
<FormField
388+
control={form.control}
389+
name={`applications.${index}.download_links.${linkIndex}.language`}
390+
render={({ field }) => (
391+
<FormItem className="w-full sm:w-24">
392+
<Select onValueChange={field.onChange} value={field.value}>
393+
<FormControl>
394+
<SelectTrigger className="h-7 text-xs">
395+
<SelectValue />
396+
</SelectTrigger>
397+
</FormControl>
398+
<SelectContent className="scrollbar-thin z-[50]">
399+
{languageOptions.map(option => (
400+
<SelectItem key={option.value} value={option.value}>
401+
<div className="flex items-center gap-1.5">
402+
<span className="text-xs">{option.icon}</span>
403+
<span className="text-xs">{option.label}</span>
404+
</div>
405+
</SelectItem>
406+
))}
407+
</SelectContent>
408+
</Select>
409+
<FormMessage />
410+
</FormItem>
411+
)}
412+
/>
413+
<Button type="button" variant="ghost" size="icon" onClick={() => removeDownloadLink(linkIndex)} className="h-7 w-7 shrink-0 p-0 text-destructive hover:bg-destructive/10">
414+
<Trash2 className="h-3 w-3" />
415+
</Button>
416+
</div>
410417
</div>
411418
)
412419
})}

0 commit comments

Comments
 (0)