Skip to content

Commit cb6a75d

Browse files
authored
Merge pull request #217 from DnsChanger/fix/tsconfig-and-shutdown-ui
Fix/tsconfig and shutdown UI
2 parents 3ce4a5d + 872f4e2 commit cb6a75d

20 files changed

Lines changed: 990 additions & 644 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ out/
9292
./store
9393

9494
dist-electron
95+
dist-tsc
9596
release
9697
dist
9798
.env

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "dnschanger",
33
"productName": "dnschanger",
4-
"version": "2.3.7",
4+
"version": "2.3.8",
55
"debug": {
66
"env": {
77
"VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/"
@@ -15,7 +15,7 @@
1515
"scripts": {
1616
"i18n": "typesafe-i18n",
1717
"dev": "vite",
18-
"build:code": "tsc && vite build",
18+
"build:code": "tsc --noEmit && vite build",
1919
"build": "vite build && electron-builder",
2020
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"",
2121
"preview": "vite preview",

public/icons/icon.icns

258 KB
Binary file not shown.

src/main/electron-env.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/// <reference types="vite-electron-plugin/electron-env" />
21

32
declare namespace NodeJS {
43
interface ProcessEnv {

src/main/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ async function createWindow() {
6464
resizable: false,
6565
center: isDev === false,
6666
show: true,
67-
68-
alwaysOnTop: true,
67+
alwaysOnTop: isDev,
6968
movable: true,
7069
frame: false,
7170
titleBarStyle: 'hidden',

src/renderer/component/buttons/benchmark-dns-btn.component.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ export function BenchmarkDnsButtonComponent({ servers }: Props) {
1515

1616
return (
1717
<div>
18-
<Tooltip content="DNS Speed Test" position="left">
18+
<Tooltip content="Find Best DNS Server" position="left">
1919
<Button
20-
size={'sm'}
20+
size="sm"
2121
onClick={() => setIsOpenModal(true)}
22-
className={'bg-base-200 hover:bg-base-200/80 rounded-xl'}
22+
className="bg-base-200 hover:bg-base-300 rounded-xl transition-all duration-200 hover:scale-105 active:scale-95 cursor-pointer"
2323
>
24-
<MdSpeed className="text-base-content/80" size={14} />
24+
<MdSpeed className="text-base-content/80" size={15} />
2525
</Button>
2626
</Tooltip>
2727

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export function ServerCardSkeletonComponent() {
2+
return (
3+
<div className="border rounded-2xl bg-base-100 border-base-300 p-3.5 shadow-sm">
4+
<div className="flex items-center gap-3">
5+
<div className="shrink-0">
6+
<div className="w-11 h-11 rounded-xl skeleton bg-base-200" />
7+
</div>
8+
9+
<div className="flex-1 min-w-0">
10+
<div className="flex items-center justify-between gap-2">
11+
<div className="flex items-center gap-2">
12+
<div className="h-4 w-28 skeleton rounded bg-base-200" />
13+
<div className="h-4 w-12 skeleton rounded-md bg-base-200" />
14+
<div className="h-4 w-12 skeleton rounded-md bg-base-200" />
15+
</div>
16+
<div className="h-4 w-14 skeleton rounded bg-base-200" />
17+
</div>
18+
19+
<div className="flex items-center justify-between mt-3 pt-2 border-t border-base-300/70">
20+
<div className="h-3 w-20 skeleton rounded bg-base-200" />
21+
<div className="h-6 w-16 skeleton rounded-xl bg-base-200" />
22+
</div>
23+
</div>
24+
</div>
25+
</div>
26+
)
27+
}
28+
29+
export { ServerCardSkeletonComponent as ServerCardSkeleton }
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import { useEffect, useState } from 'react'
2+
import {
3+
IoAddCircleOutline,
4+
IoCheckmarkCircleOutline,
5+
IoRemoveCircleOutline,
6+
} from 'react-icons/io5'
7+
import { FiCopy } from 'react-icons/fi'
8+
import { CiCircleMore } from 'react-icons/ci'
9+
import { FaRegStar, FaStar, FaStarHalfAlt } from 'react-icons/fa'
10+
11+
import type { Server } from '../../../shared/interfaces/server.interface'
12+
import { getPingIcon } from '../../utils/icons.util'
13+
14+
export interface ServerCardProps {
15+
server: Server
16+
storeServers: Server[]
17+
setStoreServers: React.Dispatch<React.SetStateAction<Server[]>>
18+
}
19+
20+
export function ServerCardComponent({
21+
server,
22+
storeServers,
23+
setStoreServers,
24+
}: ServerCardProps) {
25+
const { avatar, name, key, tags, servers, rate, ping } = server
26+
27+
const [menuOpen, setMenuOpen] = useState(false)
28+
const [actionLoading, setActionLoading] = useState(false)
29+
const [actionError, setActionError] = useState(false)
30+
const [copied, setCopied] = useState(false)
31+
32+
const isInstalled = storeServers.some((item) => item.key === key)
33+
34+
useEffect(() => {
35+
if (!menuOpen) return
36+
37+
function handleKey(e: KeyboardEvent) {
38+
if (e.key === 'Escape') setMenuOpen(false)
39+
}
40+
41+
window.addEventListener('keydown', handleKey)
42+
return () => window.removeEventListener('keydown', handleKey)
43+
}, [menuOpen])
44+
45+
async function addHandler() {
46+
setActionLoading(true)
47+
setActionError(false)
48+
49+
try {
50+
const response = await window.ipc.addDns(server)
51+
if (response.success) {
52+
setStoreServers(response.servers || [])
53+
} else {
54+
setActionError(true)
55+
}
56+
} catch {
57+
setActionError(true)
58+
} finally {
59+
setActionLoading(false)
60+
}
61+
}
62+
63+
async function deleteHandler() {
64+
setActionLoading(true)
65+
setActionError(false)
66+
67+
try {
68+
const response = await window.ipc.deleteDns(server)
69+
if (response.success) {
70+
setStoreServers(response.servers || [])
71+
} else {
72+
setActionError(true)
73+
}
74+
} catch {
75+
setActionError(true)
76+
} finally {
77+
setActionLoading(false)
78+
}
79+
}
80+
81+
function copyHandler() {
82+
navigator.clipboard.writeText(servers.join(', '))
83+
setCopied(true)
84+
setTimeout(() => setCopied(false), 1500)
85+
}
86+
87+
function renderRating(value: number) {
88+
const rating = value / 2
89+
const full = Math.floor(rating)
90+
const half = rating - full >= 0.5
91+
const stars = []
92+
93+
for (let i = 0; i < 5; i++) {
94+
if (i < full) {
95+
stars.push(<FaStar key={i} className="text-warning" size={11} />)
96+
} else if (i === full && half) {
97+
stars.push(<FaStarHalfAlt key={i} className="text-warning" size={11} />)
98+
} else {
99+
stars.push(<FaRegStar key={i} className="text-base-300" size={11} />)
100+
}
101+
}
102+
return stars
103+
}
104+
105+
return (
106+
<div className="relative transition-all duration-200 border rounded-2xl bg-base-100 border-base-300 hover:border-primary/40 hover:shadow-md">
107+
<div className="p-3.5">
108+
<div className="flex items-center gap-3">
109+
<div className="shrink-0">
110+
<div className="w-11 h-11 overflow-hidden rounded-xl bg-base-200 border border-base-300 flex items-center justify-center">
111+
<img
112+
src={`./servers-icon/${avatar}`}
113+
alt={name}
114+
className="object-cover w-full h-full"
115+
onError={({ currentTarget }) => {
116+
currentTarget.onerror = null
117+
currentTarget.src = './servers-icon/def.png'
118+
}}
119+
/>
120+
</div>
121+
</div>
122+
123+
<div className="flex-1 min-w-0">
124+
<div className="flex items-center gap-2">
125+
<h3 className="text-sm font-semibold truncate text-base-content">
126+
{name}
127+
</h3>
128+
129+
<div className="flex flex-wrap gap-1 shrink-0">
130+
{tags.slice(0, 3).map((tag: string) => (
131+
<span
132+
key={tag}
133+
className="px-1.5 py-0.5 text-[10px] font-medium rounded-md bg-base-200 border border-base-300/60 text-base-content/70 whitespace-nowrap"
134+
>
135+
{tag}
136+
</span>
137+
))}
138+
{tags.length > 3 && (
139+
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded-md bg-base-200 border border-base-300/60 text-base-content/50">
140+
+{tags.length - 3}
141+
</span>
142+
)}
143+
</div>
144+
145+
<div className="flex items-center gap-1 ml-auto shrink-0">
146+
<span className="shrink-0">{getPingIcon(ping)}</span>
147+
<span className="text-xs text-base-content/60 font-mono">
148+
{ping === -1 ? 'N/A' : `${ping}ms`}
149+
</span>
150+
</div>
151+
152+
<button
153+
onClick={() => setMenuOpen(!menuOpen)}
154+
className="p-1.5 transition-colors rounded-lg shrink-0 text-base-content/50 hover:bg-base-200 hover:text-base-content"
155+
aria-label="More options"
156+
>
157+
<CiCircleMore size={18} />
158+
</button>
159+
</div>
160+
161+
<div className="flex items-center justify-between mt-2 pt-2 border-t border-base-300/70">
162+
<div className="flex items-center gap-2">
163+
<div className="flex items-center gap-0.5">
164+
{renderRating(rate)}
165+
</div>
166+
{actionError && (
167+
<span className="text-[10px] text-error">
168+
Failed to complete operation.
169+
</span>
170+
)}
171+
</div>
172+
173+
<button
174+
onClick={isInstalled ? deleteHandler : addHandler}
175+
disabled={actionLoading}
176+
className={`flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-xl transition-all cursor-pointer duration-200 disabled:opacity-50 ${
177+
isInstalled
178+
? 'btn-error btn-outline btn-xs hover:bg-error hover:text-error-content hover:border-error hover:scale-[1.03] active:scale-[0.97] hover:shadow-sm'
179+
: 'btn-primary btn-xs hover:brightness-110 hover:scale-[1.03] active:scale-[0.97] hover:shadow-md'
180+
}`}
181+
>
182+
{actionLoading ? (
183+
<span className="loading loading-spinner loading-xs" />
184+
) : isInstalled ? (
185+
<IoRemoveCircleOutline size={14} />
186+
) : (
187+
<IoAddCircleOutline size={14} />
188+
)}
189+
{isInstalled ? 'Remove' : 'Add'}
190+
</button>
191+
</div>
192+
</div>
193+
</div>
194+
</div>
195+
196+
{/* Dropdown Menu */}
197+
{menuOpen && (
198+
<>
199+
<div
200+
className="fixed inset-0 z-10"
201+
onClick={() => setMenuOpen(false)}
202+
/>
203+
<div className="absolute z-20 w-64 p-2 border shadow-xl right-3 top-12 rounded-2xl bg-base-100 border-base-300">
204+
{/* Copy DNS */}
205+
<button
206+
onClick={copyHandler}
207+
className="flex items-center w-full gap-2 px-3 py-2 text-xs font-medium transition-colors rounded-xl text-base-content hover:bg-base-200"
208+
>
209+
{copied ? (
210+
<IoCheckmarkCircleOutline
211+
size={15}
212+
className="shrink-0 text-success"
213+
/>
214+
) : (
215+
<FiCopy
216+
size={15}
217+
className="shrink-0 text-base-content/70"
218+
/>
219+
)}
220+
<span>
221+
{copied ? 'Copied to clipboard!' : 'Copy DNS Addresses'}
222+
</span>
223+
</button>
224+
225+
{/* DNS Servers List */}
226+
<div className="px-3 py-2 mt-1.5 rounded-xl bg-base-200/80 border border-base-300/40">
227+
<div className="text-[10px] font-semibold uppercase text-base-content/50">
228+
DNS Servers
229+
</div>
230+
<div className="mt-1 space-y-0.5">
231+
{servers.map((address: string) => (
232+
<div
233+
key={address}
234+
className="font-mono text-xs truncate text-base-content/80"
235+
>
236+
{address}
237+
</div>
238+
))}
239+
</div>
240+
</div>
241+
242+
{/* Rating */}
243+
<div className="flex items-center justify-between px-3 py-1.5 mt-1">
244+
<span className="text-xs font-medium text-base-content/60">
245+
Rating
246+
</span>
247+
<div className="flex items-center gap-1.5">
248+
<div className="flex items-center gap-0.5">
249+
{renderRating(rate)}
250+
</div>
251+
<span className="text-xs font-mono text-base-content/60">
252+
{(rate / 2).toFixed(1)}
253+
</span>
254+
</div>
255+
</div>
256+
</div>
257+
</>
258+
)}
259+
</div>
260+
)
261+
}
262+
263+
export { ServerCardComponent as ServerCard }
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export interface ExploreEmptyStateProps {
2+
search: string
3+
onClearSearch: () => void
4+
}
5+
6+
export function ExploreEmptyStateComponent({
7+
search,
8+
onClearSearch,
9+
}: ExploreEmptyStateProps) {
10+
return (
11+
<div className="py-12 text-center border rounded-2xl border-base-300 bg-base-100 shadow-sm">
12+
<h2 className="font-semibold text-base text-base-content">
13+
No DNS Servers Found
14+
</h2>
15+
<p className="mt-1 text-xs text-base-content/60">
16+
{search
17+
? 'No server matches your search criteria.'
18+
: 'No DNS servers available right now.'}
19+
</p>
20+
21+
{search && (
22+
<button
23+
onClick={onClearSearch}
24+
className="mt-3 text-xs font-medium text-primary hover:underline"
25+
>
26+
Clear search filter
27+
</button>
28+
)}
29+
</div>
30+
)
31+
}

0 commit comments

Comments
 (0)