@@ -5,32 +5,54 @@ import { Dialog } from './Dialog';
55import { store } from '../store/core' ;
66import { startRemoteAccess , stopRemoteAccess , refreshRemoteStatus } from '../store/remote' ;
77import { theme } from '../lib/theme' ;
8+ import type { RemoteAccess } from '../store/types' ;
89
910type NetworkMode = 'wifi' | 'tailscale' ;
11+ type RemoteAccessUrls = Pick < RemoteAccess , 'enabled' | 'url' | 'wifiUrl' | 'tailscaleUrl' > ;
1012
1113interface ConnectPhoneModalProps {
1214 open : boolean ;
1315 onClose : ( ) => void ;
1416}
1517
18+ export function connectionUrlForMode (
19+ remoteAccess : RemoteAccessUrls ,
20+ networkMode : NetworkMode ,
21+ ) : string | null {
22+ if ( ! remoteAccess . enabled ) return null ;
23+ const modeUrl = networkMode === 'tailscale' ? remoteAccess . tailscaleUrl : remoteAccess . wifiUrl ;
24+ return modeUrl ?? remoteAccess . url ;
25+ }
26+
27+ export function availableNetworkModeFor (
28+ remoteAccess : RemoteAccessUrls ,
29+ currentMode : NetworkMode ,
30+ ) : NetworkMode {
31+ if ( currentMode === 'wifi' && remoteAccess . wifiUrl ) return 'wifi' ;
32+ if ( currentMode === 'tailscale' && remoteAccess . tailscaleUrl ) return 'tailscale' ;
33+ if ( remoteAccess . wifiUrl ) return 'wifi' ;
34+ if ( remoteAccess . tailscaleUrl ) return 'tailscale' ;
35+ return currentMode ;
36+ }
37+
1638export function ConnectPhoneModal ( props : ConnectPhoneModalProps ) {
1739 const [ qrDataUrl , setQrDataUrl ] = createSignal < string | null > ( null ) ;
40+ const [ qrError , setQrError ] = createSignal < string | null > ( null ) ;
1841 const [ starting , setStarting ] = createSignal ( false ) ;
1942 const [ error , setError ] = createSignal < string | null > ( null ) ;
2043 const [ copied , setCopied ] = createSignal ( false ) ;
2144 const [ mode , setMode ] = createSignal < NetworkMode > ( 'wifi' ) ;
2245 let stopPolling : ( ( ) => void ) | undefined ;
2346 let copiedTimer : ReturnType < typeof setTimeout > | undefined ;
47+ let qrRequestId = 0 ;
2448 onCleanup ( ( ) => {
2549 if ( copiedTimer !== undefined ) clearTimeout ( copiedTimer ) ;
50+ qrRequestId ++ ;
2651 } ) ;
2752
28- const activeUrl = createMemo ( ( ) => {
29- if ( ! store . remoteAccess . enabled ) return null ;
30- return mode ( ) === 'tailscale' ? store . remoteAccess . tailscaleUrl : store . remoteAccess . wifiUrl ;
31- } ) ;
53+ const activeUrl = createMemo ( ( ) => connectionUrlForMode ( store . remoteAccess , mode ( ) ) ) ;
3254
33- async function generateQr ( url : string ) {
55+ async function generateQr ( url : string , requestId : number ) {
3456 try {
3557 const mod = await import ( 'qrcode' ) ;
3658 // qrcode is CJS — Vite dev wraps it as .default only, prod adds named re-exports
@@ -40,20 +62,30 @@ export function ConnectPhoneModal(props: ConnectPhoneModalProps) {
4062 margin : 2 ,
4163 color : { dark : '#000000' , light : '#ffffff' } ,
4264 } ) ;
65+ if ( requestId !== qrRequestId ) return ;
4366 setQrDataUrl ( dataUrl ) ;
67+ setQrError ( null ) ;
4468 } catch ( err ) {
69+ if ( requestId !== qrRequestId ) return ;
4570 console . error ( '[ConnectPhoneModal] QR generation failed:' , err ) ;
4671 setQrDataUrl ( null ) ;
72+ setQrError ( 'QR code unavailable' ) ;
4773 }
4874 }
4975
50- // Regenerate QR when mode changes
76+ // Regenerate QR when the shown connection URL changes.
5177 createEffect ( ( ) => {
5278 const url = activeUrl ( ) ;
53- if ( url ) {
54- setQrDataUrl ( null ) ; // clear stale QR immediately
55- generateQr ( url ) ;
79+ if ( ! props . open || ! url ) {
80+ qrRequestId ++ ;
81+ setQrDataUrl ( null ) ;
82+ setQrError ( null ) ;
83+ return ;
5684 }
85+ const requestId = ++ qrRequestId ;
86+ setQrDataUrl ( null ) ;
87+ setQrError ( null ) ;
88+ generateQr ( url , requestId ) ;
5789 } ) ;
5890
5991 // Focus the dialog panel when it opens (Dialog doesn't auto-focus)
@@ -75,28 +107,25 @@ export function ConnectPhoneModal(props: ConnectPhoneModalProps) {
75107 startRemoteAccess ( )
76108 . then ( ( result ) => {
77109 setStarting ( false ) ;
78- // Default to wifi if available, otherwise tailscale
79- setMode ( result . wifiUrl ? 'wifi' : 'tailscale' ) ;
80- const url = result . wifiUrl ?? result . tailscaleUrl ?? result . url ;
81- generateQr ( url ) ;
110+ setMode (
111+ availableNetworkModeFor (
112+ {
113+ enabled : true ,
114+ url : result . url ,
115+ wifiUrl : result . wifiUrl ,
116+ tailscaleUrl : result . tailscaleUrl ,
117+ } ,
118+ untrack ( mode ) ,
119+ ) ,
120+ ) ;
82121 } )
83122 . catch ( ( err : unknown ) => {
84123 setStarting ( false ) ;
85124 setError ( err instanceof Error ? err . message : 'Failed to start server' ) ;
86125 } ) ;
87126 } else {
88127 // Re-derive mode if network changed since last open
89- if ( mode ( ) === 'wifi' && ! store . remoteAccess . wifiUrl && store . remoteAccess . tailscaleUrl ) {
90- setMode ( 'tailscale' ) ;
91- } else if (
92- mode ( ) === 'tailscale' &&
93- ! store . remoteAccess . tailscaleUrl &&
94- store . remoteAccess . wifiUrl
95- ) {
96- setMode ( 'wifi' ) ;
97- }
98- const url = activeUrl ( ) ;
99- if ( url ) generateQr ( url ) ;
128+ setMode ( availableNetworkModeFor ( store . remoteAccess , mode ( ) ) ) ;
100129 }
101130
102131 // Poll connected clients count while modal is open
@@ -224,15 +253,43 @@ export function ConnectPhoneModal(props: ConnectPhoneModalProps) {
224253 </ div >
225254
226255 { /* QR Code */ }
227- < Show when = { qrDataUrl ( ) } >
228- { ( url ) => (
229- < img
230- src = { url ( ) }
231- alt = "Connection QR code"
232- style = { { width : '200px' , height : '200px' , 'border-radius' : '8px' } }
233- />
234- ) }
235- </ Show >
256+ < div
257+ style = { {
258+ width : '200px' ,
259+ height : '200px' ,
260+ 'border-radius' : '8px' ,
261+ background : '#ffffff' ,
262+ display : 'flex' ,
263+ 'align-items' : 'center' ,
264+ 'justify-content' : 'center' ,
265+ overflow : 'hidden' ,
266+ } }
267+ >
268+ < Show
269+ when = { qrDataUrl ( ) }
270+ fallback = {
271+ < span
272+ aria-live = "polite"
273+ style = { {
274+ color : '#3f3f46' ,
275+ 'font-size' : '12px' ,
276+ 'text-align' : 'center' ,
277+ padding : '16px' ,
278+ } }
279+ >
280+ { qrError ( ) ?? 'Generating QR code...' }
281+ </ span >
282+ }
283+ >
284+ { ( url ) => (
285+ < img
286+ src = { url ( ) }
287+ alt = "Connection QR code"
288+ style = { { width : '200px' , height : '200px' } }
289+ />
290+ ) }
291+ </ Show >
292+ </ div >
236293
237294 { /* URL */ }
238295 < div
@@ -252,7 +309,7 @@ export function ConnectPhoneModal(props: ConnectPhoneModalProps) {
252309 onClick = { handleCopyUrl }
253310 title = "Click to copy"
254311 >
255- { activeUrl ( ) ?? store . remoteAccess . url }
312+ { activeUrl ( ) }
256313 </ div >
257314
258315 < Show when = { copied ( ) } >
0 commit comments