@@ -3,12 +3,40 @@ import QRCode from "qrcode"
33import { useEffect , useState , useRef } from "react"
44import { APP_CONFIG , THEMES } from "../config"
55import serverConfig from "../server-config.json"
6+ import { t } from "../utils/i18n"
67export 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+
1029function 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
0 commit comments