@@ -917,6 +917,20 @@ export async function uploadFile(
917917}
918918
919919export async function downloadAttachment ( serverId : string , attachmentId : string ) : Promise < void > {
920+ // Native (iOS): skip blob fetch — WKWebView can't trigger downloads from blob URLs.
921+ // Get a one-time token and open in system browser which handles save natively.
922+ const isNative = ! ! ( globalThis as Record < string , unknown > ) . Capacitor ;
923+ if ( isNative ) {
924+ const tokenRes = await apiFetch ( `/api/server/${ serverId } /uploads/${ attachmentId } /download-token` , { method : 'POST' } ) ;
925+ const downloadToken = ( tokenRes as { token : string } ) . token ;
926+ const baseUrl = _baseUrl || window . location . origin ;
927+ const downloadUrl = `${ baseUrl } /api/server/${ serverId } /uploads/${ attachmentId } /download?token=${ downloadToken } ` ;
928+ const { Browser } = await import ( '@capacitor/browser' ) ;
929+ await Browser . open ( { url : downloadUrl } ) ;
930+ return ;
931+ }
932+
933+ // Desktop: fetch blob and trigger <a download>
920934 const res = await rawFetch ( `/api/server/${ serverId } /uploads/${ attachmentId } /download` ) ;
921935 if ( ! res . ok ) {
922936 const body = await res . text ( ) . catch ( ( ) => '' ) ;
@@ -926,7 +940,6 @@ export async function downloadAttachment(serverId: string, attachmentId: string)
926940 const disposition = res . headers . get ( 'Content-Disposition' ) ;
927941 let filename = attachmentId ;
928942 if ( disposition ) {
929- // Prefer filename* (RFC 5987) for non-ASCII names
930943 const starMatch = disposition . match ( / f i l e n a m e \* \s * = \s * U T F - 8 ' ' ( [ ^ \s ; ] + ) / i) ;
931944 if ( starMatch ) {
932945 try { filename = decodeURIComponent ( starMatch [ 1 ] ) ; } catch { /* keep default */ }
@@ -935,27 +948,14 @@ export async function downloadAttachment(serverId: string, attachmentId: string)
935948 if ( plainMatch ) filename = plainMatch [ 1 ] ;
936949 }
937950 }
938- // Trigger download — WKWebView (iOS Capacitor) ignores <a download>,
939- // so on native platforms get a one-time download token and open the URL
940- // in the system browser. No extra native plugins needed.
941- const isNative = ! ! ( globalThis as Record < string , unknown > ) . Capacitor ;
942- if ( isNative ) {
943- const tokenRes = await apiFetch ( `/api/server/${ serverId } /uploads/${ attachmentId } /download-token` , { method : 'POST' } ) ;
944- const downloadToken = ( tokenRes as { token : string } ) . token ;
945- const baseUrl = _baseUrl || window . location . origin ;
946- const downloadUrl = `${ baseUrl } /api/server/${ serverId } /uploads/${ attachmentId } /download?token=${ downloadToken } ` ;
947- const { Browser } = await import ( '@capacitor/browser' ) ;
948- await Browser . open ( { url : downloadUrl } ) ;
949- } else {
950- const url = URL . createObjectURL ( blob ) ;
951- const a = document . createElement ( 'a' ) ;
952- a . href = url ;
953- a . download = filename ;
954- document . body . appendChild ( a ) ;
955- a . click ( ) ;
956- document . body . removeChild ( a ) ;
957- URL . revokeObjectURL ( url ) ;
958- }
951+ const url = URL . createObjectURL ( blob ) ;
952+ const a = document . createElement ( 'a' ) ;
953+ a . href = url ;
954+ a . download = filename ;
955+ document . body . appendChild ( a ) ;
956+ a . click ( ) ;
957+ document . body . removeChild ( a ) ;
958+ URL . revokeObjectURL ( url ) ;
959959}
960960
961961export async function previewAttachment ( serverId : string , attachmentId : string ) : Promise < void > {
0 commit comments