Skip to content

Commit b83f7b9

Browse files
committed
feat(Proxies): implement display mode toggle for proxy nodes and add ProxyNodeListItem component
1 parent 9ee4aec commit b83f7b9

File tree

10 files changed

+290
-16
lines changed

10 files changed

+290
-16
lines changed

src/components/Collapse.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { JSX, ParentComponent } from 'solid-js'
22
import { twMerge } from 'tailwind-merge'
3+
import { PROXIES_DISPLAY_MODE } from '~/constants'
4+
import { proxiesDisplayMode } from '~/signals'
35

46
type Props = {
57
title: JSX.Element
@@ -24,6 +26,8 @@ export const Collapse: ParentComponent<Props> = (props) => {
2426
return props.isOpen ? openedClassName : closedClassName
2527
}
2628

29+
const isListMode = () => proxiesDisplayMode() === PROXIES_DISPLAY_MODE.LIST
30+
2731
return (
2832
<div
2933
class={twMerge(
@@ -41,9 +45,14 @@ export const Collapse: ParentComponent<Props> = (props) => {
4145
<div
4246
class={twMerge(
4347
getCollapseContentClassName(),
44-
'collapse-content grid gap-2 transition-opacity duration-1000',
48+
'collapse-content transition-opacity duration-1000',
49+
isListMode() ? 'flex flex-col gap-1' : 'grid gap-2',
4550
)}
46-
style="grid-template-columns: repeat(auto-fill, minmax(150px, 1fr))"
51+
style={
52+
isListMode()
53+
? undefined
54+
: 'grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))'
55+
}
4756
>
4857
<Show when={props.isOpen}>{children(() => props.children)()}</Show>
4958
</div>

src/components/ProxiesSettingsModal.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { IconGlobe } from '@tabler/icons-solidjs'
22
import type { Component } from 'solid-js'
33
import { ConfigTitle, Modal } from '~/components'
4-
import { PROXIES_ORDERING_TYPE, PROXIES_PREVIEW_TYPE } from '~/constants'
4+
import {
5+
PROXIES_DISPLAY_MODE,
6+
PROXIES_ORDERING_TYPE,
7+
PROXIES_PREVIEW_TYPE,
8+
} from '~/constants'
59
import { useI18n } from '~/i18n'
610
import {
711
autoCloseConns,
812
hideUnAvailableProxies,
913
iconHeight,
1014
iconMarginRight,
1115
latencyTestTimeoutDuration,
16+
proxiesDisplayMode,
1217
proxiesOrderingType,
1318
proxiesPreviewType,
1419
renderProxiesInTwoColumns,
@@ -17,6 +22,7 @@ import {
1722
setIconHeight,
1823
setIconMarginRight,
1924
setLatencyTestTimeoutDuration,
25+
setProxiesDisplayMode,
2026
setProxiesOrderingType,
2127
setProxiesPreviewType,
2228
setRenderProxiesInTwoColumns,
@@ -120,6 +126,22 @@ export const ProxiesSettingsModal: Component<{
120126
</div>
121127
</div>
122128

129+
<div>
130+
<ConfigTitle withDivider>{t('proxiesDisplayMode')}</ConfigTitle>
131+
132+
<select
133+
class="select w-full"
134+
value={proxiesDisplayMode()}
135+
onChange={(e) =>
136+
setProxiesDisplayMode(e.target.value as PROXIES_DISPLAY_MODE)
137+
}
138+
>
139+
<For each={Object.values(PROXIES_DISPLAY_MODE)}>
140+
{(value) => <option value={value}>{t(value)}</option>}
141+
</For>
142+
</select>
143+
</div>
144+
123145
<div>
124146
<ConfigTitle withDivider>{t('proxiesPreviewType')}</ConfigTitle>
125147

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import Tooltip from '@corvu/tooltip'
2+
import { IconCircleCheckFilled } from '@tabler/icons-solidjs'
3+
import dayjs from 'dayjs'
4+
import { twMerge } from 'tailwind-merge'
5+
import { Latency } from '~/components'
6+
import {
7+
filterSpecialProxyType,
8+
formatProxyType,
9+
getLatencyClassName,
10+
} from '~/helpers'
11+
import { useI18n } from '~/i18n'
12+
import { rootElement, useProxies } from '~/signals'
13+
14+
export const ProxyNodeListItem = (props: {
15+
proxyName: string
16+
testUrl: string | null
17+
timeout: number | null
18+
isSelected?: boolean
19+
onClick?: () => void
20+
}) => {
21+
const { proxyName, isSelected, onClick } = props
22+
const [t] = useI18n()
23+
const {
24+
proxyNodeMap,
25+
proxyLatencyTest,
26+
proxyLatencyTestingMap,
27+
getLatencyHistoryByName,
28+
} = useProxies()
29+
const proxyNode = createMemo(() => proxyNodeMap()[proxyName])
30+
31+
const specialTypes = createMemo(() => {
32+
if (!filterSpecialProxyType(proxyNode()?.type)) return null
33+
34+
return [
35+
proxyNode().xudp && 'xudp',
36+
proxyNode().udp && 'udp',
37+
proxyNode().tfo && 'TFO',
38+
]
39+
.filter(Boolean)
40+
.join(' / ')
41+
})
42+
43+
const isUDP = createMemo(() => proxyNode().xudp || proxyNode().udp)
44+
45+
const latencyTestHistory = getLatencyHistoryByName(
46+
props.proxyName,
47+
props.testUrl,
48+
)
49+
50+
return (
51+
<Tooltip
52+
placement="top"
53+
group="proxy-node"
54+
openDelay={300}
55+
floatingOptions={{
56+
flip: true,
57+
shift: { padding: 8 },
58+
offset: 8,
59+
arrow: 8,
60+
size: { fitViewPort: true },
61+
}}
62+
>
63+
<Tooltip.Anchor
64+
as="div"
65+
class={twMerge(
66+
'rounded-lg bg-neutral text-neutral-content',
67+
isSelected && 'bg-primary text-primary-content',
68+
)}
69+
>
70+
<Tooltip.Trigger as="div">
71+
<div
72+
class={twMerge(
73+
'flex items-center gap-2 px-3 py-1.5',
74+
onClick && 'cursor-pointer hover:opacity-80',
75+
)}
76+
onClick={onClick}
77+
>
78+
{/* Selected indicator */}
79+
<Show when={isSelected}>
80+
<IconCircleCheckFilled class="size-4 shrink-0" />
81+
</Show>
82+
83+
{/* Proxy name */}
84+
<span class="min-w-0 flex-1 truncate text-sm font-medium">
85+
{proxyName}
86+
</span>
87+
88+
{/* UDP indicator */}
89+
<Show when={isUDP()}>
90+
<span class="badge shrink-0 badge-xs badge-info">U</span>
91+
</Show>
92+
93+
{/* Special types */}
94+
<Show when={specialTypes()}>
95+
<span class="hidden text-xs uppercase opacity-60 sm:inline">
96+
{specialTypes()}
97+
</span>
98+
</Show>
99+
100+
{/* Proxy type */}
101+
<span class="hidden text-xs uppercase opacity-75 sm:inline">
102+
{formatProxyType(proxyNode()?.type)}
103+
</span>
104+
105+
{/* Latency */}
106+
<Latency
107+
proxyName={props.proxyName}
108+
testUrl={props.testUrl || null}
109+
class={twMerge(
110+
'shrink-0',
111+
proxyLatencyTestingMap()[proxyName] && 'animate-pulse',
112+
)}
113+
onClick={(e) => {
114+
e.stopPropagation()
115+
116+
void proxyLatencyTest(
117+
proxyName,
118+
proxyNode().provider,
119+
props.testUrl,
120+
props.timeout,
121+
)
122+
}}
123+
/>
124+
</div>
125+
</Tooltip.Trigger>
126+
127+
<Tooltip.Portal mount={rootElement()}>
128+
<Tooltip.Content class="z-50">
129+
<Tooltip.Arrow class="text-primary [&>svg]:-translate-y-px [&>svg]:fill-current" />
130+
131+
<div class="flex max-h-[70vh] flex-col items-center gap-2 overflow-y-auto rounded-box bg-primary p-2.5 text-primary-content shadow-lg [clip-path:inset(0_round_var(--radius-box))]">
132+
<h2 class="text-lg font-bold">{proxyName}</h2>
133+
134+
<Show when={specialTypes()}>
135+
<div class="w-full text-xs uppercase">{specialTypes()}</div>
136+
</Show>
137+
138+
<Show
139+
when={latencyTestHistory.length > 0}
140+
fallback={
141+
<div class="text-sm opacity-75">{t('noLatencyHistory')}</div>
142+
}
143+
>
144+
<ul class="timeline timeline-vertical timeline-compact timeline-snap-icon">
145+
<For each={latencyTestHistory}>
146+
{(latencyTestResult, index) => (
147+
<li>
148+
<Show when={index() > 0}>
149+
<hr />
150+
</Show>
151+
152+
<div class="timeline-start space-y-2">
153+
<time class="text-sm italic">
154+
{dayjs(latencyTestResult.time).format(
155+
'YYYY-MM-DD HH:mm:ss',
156+
)}
157+
</time>
158+
159+
<div
160+
class={twMerge(
161+
'badge block',
162+
getLatencyClassName(latencyTestResult.delay),
163+
)}
164+
>
165+
{latencyTestResult.delay || '---'}
166+
</div>
167+
</div>
168+
169+
<div class="timeline-middle">
170+
<IconCircleCheckFilled class="size-4" />
171+
</div>
172+
173+
<Show when={index() !== latencyTestHistory.length - 1}>
174+
<hr />
175+
</Show>
176+
</li>
177+
)}
178+
</For>
179+
</ul>
180+
</Show>
181+
</div>
182+
</Tooltip.Content>
183+
</Tooltip.Portal>
184+
</Tooltip.Anchor>
185+
</Tooltip>
186+
)
187+
}

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './LogsSettingsModal'
1515
export * from './Modal'
1616
export * from './ProxiesSettingsModal'
1717
export * from './ProxyNodeCard'
18+
export * from './ProxyNodeListItem'
1819
export * from './ProxyNodePreview'
1920
export * from './ProxyPreviewBar'
2021
export * from './ProxyPreviewDots'

src/constants/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ export enum PROXIES_ORDERING_TYPE {
7272
NAME_DESC = 'orderName_desc',
7373
}
7474

75+
export enum PROXIES_DISPLAY_MODE {
76+
CARD = 'cardMode',
77+
LIST = 'listMode',
78+
}
79+
7580
export enum LANG {
7681
EN = 'en-US',
7782
ZH = 'zh-CN',

src/i18n/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ export default {
5050
auto: 'Auto',
5151
off: 'Off',
5252
proxiesPreviewType: 'Proxies Preview Type',
53+
proxiesDisplayMode: 'Display Mode',
54+
cardMode: 'Card',
55+
listMode: 'List',
5356
urlForLatencyTest: 'URL for Latency Test',
5457
autoCloseConns: 'Automatically Close Connections',
5558
autoSwitchTheme: 'Automatically switch theme',

src/i18n/ru.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export default {
5252
auto: 'Авто',
5353
off: 'Выключено',
5454
proxiesPreviewType: 'Тип предварительного просмотра прокси',
55+
proxiesDisplayMode: 'Режим отображения',
56+
cardMode: 'Карточки',
57+
listMode: 'Список',
5558
urlForLatencyTest: 'URL для теста задержки',
5659
autoCloseConns: 'Автоматически закрывать соединения',
5760
autoSwitchTheme: 'Автоматически переключать тему',

src/i18n/zh.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export default {
5252
auto: '自适应',
5353
off: '关闭',
5454
proxiesPreviewType: '节点组预览样式',
55+
proxiesDisplayMode: '节点显示模式',
56+
cardMode: '卡片',
57+
listMode: '列表',
5558
urlForLatencyTest: '测速链接',
5659
autoCloseConns: '自动断开连接',
5760
autoSwitchTheme: '自动切换主题',

0 commit comments

Comments
 (0)