88 type PrinterRenderDims ,
99} from '../../lib/printerPreview' ;
1010import { getPreviewTransport , getPrinterAddress , getUsbPrinterId } from '../../lib/printerAddress' ;
11- import { isMacDesktop } from '../../lib/platform' ;
1211import { buildActiveCsvRow } from '../../lib/variableBinding' ;
1312import { buildPreviewZpl } from '../../lib/printPreview' ;
1413import { currentObjects , selectEffectivePreviewProvider , selectLabelaryEndpoint } from '../labelStore.selectors' ;
@@ -52,22 +51,19 @@ const previewCache = (() => {
5251export const __resetPreviewCacheForTests = ( ) : void => previewCache . _resetForTests ( ) ;
5352
5453/** The configured preview target, or the message telling the user what to
55- * configure. USB and network live in separate settings so an incomplete one
56- * never silently falls back to the other. */
54+ * configure. */
5755function resolvePreviewTarget ( ) : { target : PreviewTarget } | { error : string } {
58- // USB query is macOS-only, and the settings UI only offers it there; gate the
59- // same way so a persisted 'usb' choice carried onto another OS falls back to
60- // the network address instead of routing to a command that always errors .
61- if ( isMacDesktop && getPreviewTransport ( ) === 'usb' ) {
56+ // Keep a persisted 'usb' choice even if the device is gone: enterPreviewMode
57+ // falls back to network at fetch time, so a re-plug restores it. An empty
58+ // selection falls through to network .
59+ if ( getPreviewTransport ( ) === 'usb' ) {
6260 const id = getUsbPrinterId ( ) ;
63- return id
64- ? { target : { kind : 'usb' , id } }
65- : { error : 'No USB printer selected. Pick one under Settings, Preview.' } ;
61+ if ( id ) return { target : { kind : 'usb' , id } } ;
6662 }
6763 const { host, port } = getPrinterAddress ( ) ;
6864 return host
6965 ? { target : { kind : 'network' , host, port } }
70- : { error : 'No printer address configured. Set the IP under Settings, Preview.' } ;
66+ : { error : 'No printer configured. Set a USB device or IP under Settings, Preview.' } ;
7167}
7268
7369/** Cache-key part identifying the device, so switching the preview transport
@@ -78,8 +74,7 @@ function previewTargetKey(target: PreviewTarget): string {
7874
7975export interface PreviewSlice {
8076 previewMode : PreviewMode ;
81- /** Render current page → fetch → set status. Caller-checked: only
82- * call when `previewMode.status` is `idle` or `error`. */
77+ /** Caller-checked: only call when `previewMode.status` is `idle` or `error`. */
8378 enterPreviewMode : ( ) => Promise < void > ;
8479 /** Reset to `idle`; blob URL stays cached for re-toggle. */
8580 exitPreviewMode : ( ) => void ;
@@ -122,14 +117,16 @@ export const createPreviewSlice: StateCreator<LabelState, [], [], PreviewSlice>
122117 // render; the printer path folds the target in instead. NUL-joined so a ':'
123118 // inside any field can't shift a boundary and collide.
124119 const endpoint = selectLabelaryEndpoint ( state ) ;
120+ const printerKey = ( t : PreviewTarget ) : string => [ provider , previewTargetKey ( t ) , zpl ] . join ( '\0' ) ;
121+ const serveCached = ( k : string ) : boolean => {
122+ const hit = previewCache . get ( k ) ;
123+ if ( hit ) set ( { previewMode : { status : 'active' , ...hit } } ) ;
124+ return hit !== null ;
125+ } ;
125126 const cacheKey = printerTarget
126- ? [ provider , previewTargetKey ( printerTarget ) , zpl ] . join ( '\0' )
127+ ? printerKey ( printerTarget )
127128 : [ provider , endpoint . host , endpoint . apiKey ?? '' , zpl ] . join ( '\0' ) ;
128- const cached = previewCache . get ( cacheKey ) ;
129- if ( cached !== null ) {
130- set ( { previewMode : { status : 'active' , ...cached } } ) ;
131- return ;
132- }
129+ if ( serveCached ( cacheKey ) ) return ;
133130 set ( { previewMode : { status : 'loading' } } ) ;
134131 // Stale-request guard: status check catches an exit mid-fetch; the
135132 // reference-equality check catches re-entry with a different design
@@ -147,8 +144,22 @@ export const createPreviewSlice: StateCreator<LabelState, [], [], PreviewSlice>
147144 const fail = ( error : string ) : void => {
148145 if ( ! isStale ( ) ) set ( { previewMode : { status : 'error' , error } } ) ;
149146 } ;
150- const result = await fetchPrinterPreview ( printerTarget , zpl ) ;
147+ let target = printerTarget ;
148+ let key = cacheKey ;
149+ let result = await fetchPrinterPreview ( target , zpl ) ;
151150 if ( isStale ( ) ) return ;
151+ // USB device gone: preview over network without touching the persisted
152+ // USB choice (resolvePreviewTarget explains why).
153+ if ( target . kind === 'usb' && result . kind === 'not_found' ) {
154+ const net = getPrinterAddress ( ) ;
155+ if ( net . host ) {
156+ target = { kind : 'network' , host : net . host , port : net . port } ;
157+ key = printerKey ( target ) ;
158+ if ( serveCached ( key ) ) return ;
159+ result = await fetchPrinterPreview ( target , zpl ) ;
160+ if ( isStale ( ) ) return ;
161+ }
162+ }
152163 switch ( result . kind ) {
153164 case 'bitmap' : {
154165 const url = bitmapToDataUrl ( result . bitmap ) ;
@@ -160,16 +171,15 @@ export const createPreviewSlice: StateCreator<LabelState, [], [], PreviewSlice>
160171 url,
161172 printerDims : printerRenderDims ( result . bitmap ) ,
162173 } ;
163- previewCache . set ( cacheKey , render ) ;
174+ previewCache . set ( key , render ) ;
164175 set ( { previewMode : { status : 'active' , ...render } } ) ;
165176 return ;
166177 }
167178 case 'refused' : {
168- // 'refused' only arises from the network transport (USB has no such
169- // kind), so the port hint is appended whenever a network target is
170- // present rather than duplicated into an unreachable USB message.
179+ // 'refused' is network-only (USB has no such kind), so gate the port
180+ // hint on a network target.
171181 const hint =
172- printerTarget . kind === 'network' ? ` Check that port ${ printerTarget . port } is open.` : '' ;
182+ target . kind === 'network' ? ` Check that port ${ target . port } is open.` : '' ;
173183 fail ( `The printer refused the connection.${ hint } ` ) ;
174184 return ;
175185 }
@@ -179,6 +189,9 @@ export const createPreviewSlice: StateCreator<LabelState, [], [], PreviewSlice>
179189 case 'not_found' :
180190 fail ( 'USB printer not found. Re-plug it and check Settings, Preview.' ) ;
181191 return ;
192+ case 'permission_denied' :
193+ fail ( 'No access to the USB printer. Grant it in the print dialog, USB tab.' ) ;
194+ return ;
182195 case 'error' :
183196 fail ( result . message ) ;
184197 return ;
0 commit comments