Skip to content

Commit 0a1455f

Browse files
authored
Merge pull request #360 from Dotify71/feat/copy-to-clipboard
feat: add one-click copy to clipboard for remote connection URL
2 parents 753c6f5 + c8b13d2 commit 0a1455f

2 files changed

Lines changed: 85 additions & 6 deletions

File tree

src/routes/settings.tsx

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,40 @@ import QRCode from "qrcode"
33
import { useEffect, useState, useRef } from "react"
44
import { APP_CONFIG, THEMES } from "../config"
55
import serverConfig from "../server-config.json"
6+
import { t } from "../utils/i18n"
67
export const Route = createFileRoute("/settings")({
78
component: SettingsPage,
89
})
910

11+
const copyWithFallback = (text: string) => {
12+
const textArea = document.createElement("textarea")
13+
textArea.value = text
14+
textArea.setAttribute("readonly", "")
15+
textArea.style.position = "absolute"
16+
textArea.style.left = "-9999px"
17+
18+
document.body.appendChild(textArea)
19+
textArea.select()
20+
textArea.setSelectionRange(0, text.length)
21+
22+
try {
23+
return document.execCommand("copy")
24+
} finally {
25+
document.body.removeChild(textArea)
26+
}
27+
}
28+
1029
function SettingsPage() {
1130
const [ip, setIp] = useState("")
31+
const [copied, setCopied] = useState(false)
32+
const [copyError, setCopyError] = useState("")
33+
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
34+
35+
useEffect(() => {
36+
return () => {
37+
if (copyTimerRef.current) clearTimeout(copyTimerRef.current)
38+
}
39+
}, [])
1240
const [frontendPort, setFrontendPort] = useState("")
1341
const [originalPort] = useState(String(serverConfig.frontendPort))
1442
const serverConfigChanged =
@@ -356,12 +384,57 @@ function SettingsPage() {
356384
</div>
357385
)}
358386

359-
<a
360-
className="link link-primary mt-2 break-all text-lg font-mono bg-base-100 px-4 py-2 rounded-lg inline-block max-w-full overflow-hidden text-ellipsis"
361-
href={shareUrl}
362-
>
363-
{shareUrl.replace(`${protocol}//`, "")}
364-
</a>
387+
<div className="flex flex-col gap-2 mt-2 w-full px-4 items-center">
388+
<a
389+
className="link link-primary break-all text-lg font-mono bg-base-100 px-4 py-2 rounded-lg inline-block max-w-full overflow-hidden text-ellipsis"
390+
href={shareUrl}
391+
>
392+
{shareUrl.replace(`${protocol}//`, "")}
393+
</a>
394+
<button
395+
type="button"
396+
className="btn btn-sm btn-outline w-full max-w-xs"
397+
onClick={async () => {
398+
setCopyError("")
399+
400+
try {
401+
if (
402+
window.isSecureContext &&
403+
navigator.clipboard?.writeText
404+
) {
405+
await navigator.clipboard.writeText(shareUrl)
406+
} else if (!copyWithFallback(shareUrl)) {
407+
throw new Error("Clipboard copy failed")
408+
}
409+
410+
setCopied(true)
411+
if (copyTimerRef.current)
412+
clearTimeout(copyTimerRef.current)
413+
copyTimerRef.current = setTimeout(() => {
414+
setCopied(false)
415+
copyTimerRef.current = null
416+
}, 2000)
417+
} catch (err) {
418+
console.error("Failed to copy URL:", err)
419+
if (copyTimerRef.current) {
420+
clearTimeout(copyTimerRef.current)
421+
copyTimerRef.current = null
422+
}
423+
setCopied(false)
424+
setCopyError(t("settings", "copyFailed"))
425+
}
426+
}}
427+
>
428+
{copied
429+
? t("settings", "copied")
430+
: t("settings", "copyLink")}
431+
</button>
432+
{copyError && (
433+
<p className="text-error text-xs text-center max-w-xs">
434+
{copyError}
435+
</p>
436+
)}
437+
</div>
365438
</div>
366439
</div>
367440

src/utils/i18n.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
export const i18n = {
66
en: {
7+
settings: {
8+
copyLink: "Copy Link",
9+
copied: "Copied!",
10+
copyFailed:
11+
"Could not copy the link automatically. Please copy it manually.",
12+
},
713
screenMirror: {
814
ariaLabel: "Remote desktop screen share",
915
connecting: "Connecting to host...",

0 commit comments

Comments
 (0)