@@ -24,7 +24,7 @@ declare global {
2424 setConsumer : (
2525 consumer : ( launchParams : {
2626 files : FileSystemFileHandle [ ] | File [ ] ;
27- } ) => void
27+ } ) => void ,
2828 ) => void ;
2929 } ;
3030 }
@@ -71,7 +71,7 @@ export interface GaplessInfo {
7171 */
7272async function compressAlbumArt (
7373 base64 : string ,
74- maxSize = 200
74+ maxSize = 200 ,
7575) : Promise < string > {
7676 // Check if it's an animated format
7777 const isAnimatedFormat =
@@ -119,7 +119,7 @@ async function compressAlbumArt(
119119 const compressed = canvas . toDataURL ( "image/jpeg" , 0.7 ) ;
120120
121121 console . log (
122- `Album art compressed: ${ ( base64 . length / 1024 ) . toFixed ( 1 ) } KB → ${ ( compressed . length / 1024 ) . toFixed ( 1 ) } KB`
122+ `Album art compressed: ${ ( base64 . length / 1024 ) . toFixed ( 1 ) } KB → ${ ( compressed . length / 1024 ) . toFixed ( 1 ) } KB` ,
123123 ) ;
124124
125125 // Clean up the canvas to free memory
@@ -293,15 +293,15 @@ async function withTimeoutAndRetry<T>(
293293 promise : Promise < T > ,
294294 timeoutMs : number ,
295295 retries : number ,
296- errorMessage : string
296+ errorMessage : string ,
297297) : Promise < T > {
298298 let lastError : Error | null = null ;
299299 for ( let attempt = 1 ; attempt <= retries ; attempt ++ ) {
300300 try {
301301 const timeoutPromise = new Promise < T > ( ( _ , reject ) => {
302302 setTimeout (
303303 ( ) => reject ( new Error ( `Timeout: ${ errorMessage } ` ) ) ,
304- timeoutMs
304+ timeoutMs ,
305305 ) ;
306306 } ) ;
307307 return await Promise . race ( [ promise , timeoutPromise ] ) ;
@@ -339,12 +339,12 @@ export function pickAudioFiles(): Promise<AudioFile[]> {
339339 "audio/*" ,
340340 ] ,
341341 } ,
342- } )
342+ } ) ,
343343 ) ;
344344
345345 const [ open , setOpen ] = useState ( true ) ; // modal open state
346346 const [ theme , setTheme ] = useState < "light" | "dark" > (
347- document . documentElement . classList . contains ( "dark" ) ? "dark" : "light"
347+ document . documentElement . classList . contains ( "dark" ) ? "dark" : "light" ,
348348 ) ;
349349
350350 // Watch for theme changes
@@ -466,7 +466,7 @@ function processFiles(files: File[]): AudioFile[] {
466466 if ( canPlay !== "probably" && canPlay !== "maybe" ) {
467467 // TODO: i18n-ize
468468 toast . error (
469- `Skipping unsupported audio format by browser: ${ file . name } (${ file . type } )`
469+ `Skipping unsupported audio format by browser: ${ file . name } (${ file . type } )` ,
470470 ) ;
471471 continue ;
472472 }
@@ -622,7 +622,7 @@ export async function extractAudioMetadata(file: File): Promise<AudioMetadata> {
622622 } ) ,
623623 15000 ,
624624 3 ,
625- `Metadata extraction for ${ file . name } `
625+ `Metadata extraction for ${ file . name } ` ,
626626 ) ;
627627 return result ;
628628 } catch ( e ) {
@@ -641,7 +641,7 @@ export async function extractAudioMetadata(file: File): Promise<AudioMetadata> {
641641 parseBlob ( file , { skipCovers : false , duration : true } ) ,
642642 15000 ,
643643 3 ,
644- `Fallback metadata parsing for ${ file . name } `
644+ `Fallback metadata parsing for ${ file . name } ` ,
645645 ) ;
646646
647647 if ( metadata . common ) {
@@ -716,7 +716,7 @@ export interface FileHandlerResult {
716716}
717717
718718export function setupFileHandler (
719- onFilesReceived : ( result : FileHandlerResult ) => void
719+ onFilesReceived : ( result : FileHandlerResult ) => void ,
720720) : ( ) => void {
721721 if (
722722 ! ( "launchQueue" in window ) ||
@@ -750,7 +750,7 @@ export function setupFileHandler(
750750
751751 // Check sessionStorage for processed files
752752 const processedFiles = JSON . parse (
753- sessionStorage . getItem ( "processedFiles" ) || "[]"
753+ sessionStorage . getItem ( "processedFiles" ) || "[]" ,
754754 ) ;
755755 if ( processedFiles . includes ( fileId ) ) {
756756 console . log ( "Skipping duplicate file:" , file . name ) ;
@@ -763,7 +763,7 @@ export function setupFileHandler(
763763 processedFiles . push ( fileId ) ;
764764 sessionStorage . setItem (
765765 "processedFiles" ,
766- JSON . stringify ( processedFiles )
766+ JSON . stringify ( processedFiles ) ,
767767 ) ;
768768 successCount ++ ;
769769 } else {
@@ -808,6 +808,32 @@ export interface ShareTargetResult {
808808 type : "files" | "search" | "none" ;
809809}
810810
811+ const SHARE_HANDLED_KEY = "last-shared-files" ;
812+
813+ // Helper functions for duplicate tracking
814+ function getHandledShares ( ) : Set < string > {
815+ try {
816+ return new Set (
817+ JSON . parse ( sessionStorage . getItem ( SHARE_HANDLED_KEY ) || "[]" ) ,
818+ ) ;
819+ } catch {
820+ return new Set ( ) ;
821+ }
822+ }
823+
824+ function markHandled ( files : File [ ] ) {
825+ const handled = getHandledShares ( ) ;
826+ files . forEach ( ( f ) => handled . add ( `${ f . name } :${ f . size } ` ) ) ;
827+ sessionStorage . setItem (
828+ SHARE_HANDLED_KEY ,
829+ JSON . stringify ( Array . from ( handled ) ) ,
830+ ) ;
831+ }
832+
833+ export function clearHandledShares ( ) {
834+ sessionStorage . removeItem ( SHARE_HANDLED_KEY ) ;
835+ }
836+
811837export function handleShareTarget ( ) : ShareTargetResult | null {
812838 // Share targets can send data via URL parameters or launch queue
813839 if ( typeof window === "undefined" ) {
@@ -838,7 +864,7 @@ export function handleShareTarget(): ShareTargetResult | null {
838864
839865// Hook to handle share targets and file shares
840866export function useShareTarget (
841- onShareReceived : ( result : ShareTargetResult ) => void
867+ onShareReceived : ( result : ShareTargetResult ) => void ,
842868) {
843869 const hasProcessedRef = useRef ( false ) ;
844870
@@ -848,32 +874,48 @@ export function useShareTarget(
848874 async function processShare ( ) {
849875 const urlParams = new URLSearchParams ( window . location . search ) ;
850876
851- // Check for files in Cache (sent from Service Worker)
877+ // Multi-file support: attempt to load from cache with indexes
852878 if ( urlParams . get ( "share-received" ) === "true" ) {
853879 try {
854880 const cache = await caches . open ( "incoming-shares" ) ;
855- const response = await cache . match ( "/shared-file" ) ;
856-
857- if ( response ) {
881+ const files : File [ ] = [ ] ;
882+ // read /shared-file, /shared-file-1, /shared-file-2, etc
883+ let i = 0 ,
884+ foundAny = false ;
885+ while ( true ) {
886+ const key = i === 0 ? "/shared-file" : `/shared-file-${ i } ` ;
887+ const response = await cache . match ( key ) ;
888+ if ( ! response ) break ;
858889 const blob = await response . blob ( ) ;
859890 const filenameRaw = response . headers . get ( "x-file-name" ) ;
860891 const filename = filenameRaw
861892 ? decodeURIComponent ( filenameRaw )
862- : "shared-audio.mp3" ;
863- const file = new File ( [ blob ] , filename , { type : blob . type } ) ;
893+ : `shared-audio${ i ? "-" + i : "" } .mp3` ;
894+ files . push ( new File ( [ blob ] , filename , { type : blob . type } ) ) ;
895+ await cache . delete ( key ) ;
896+ foundAny = true ;
897+ i ++ ;
898+ }
899+
900+ if ( foundAny ) {
901+ const handled = getHandledShares ( ) ;
902+ const newFiles = files . filter (
903+ ( f ) => ! handled . has ( `${ f . name } :${ f . size } ` ) ,
904+ ) ;
905+ if ( newFiles . length === 0 ) {
906+ // all were previously handled
907+ return ;
908+ }
909+ markHandled ( newFiles ) ;
864910
865911 hasProcessedRef . current = true ;
866- onShareReceived ( {
867- files : [ file ] ,
868- type : "files" ,
869- } ) ;
912+ onShareReceived ( { files : newFiles , type : "files" } ) ;
870913
871- // Cleanup
872- await cache . delete ( "/shared-file" ) ;
914+ // cleanup share-received param
873915 const cleanUrl = new URL ( window . location . href ) ;
874916 cleanUrl . searchParams . delete ( "share-received" ) ;
875917 window . history . replaceState ( { } , "" , cleanUrl . toString ( ) ) ;
876- return ; // Exit early if we handled files
918+ return ;
877919 }
878920 } catch ( e ) {
879921 console . error ( "Failed to retrieve shared file from cache" , e ) ;
@@ -899,7 +941,7 @@ export function useShareTarget(
899941export function useFileHandler (
900942 addSong : ( song : Song ) => Promise < void > ,
901943 t : any ,
902- isInitialized ?: boolean
944+ isInitialized ?: boolean ,
903945) {
904946 const [ isSupported , setIsSupported ] = useState ( false ) ;
905947 const processedFilesRef = useRef ( new Set < string > ( ) ) ;
@@ -922,23 +964,23 @@ export function useFileHandler(
922964 // If library is not initialized yet, queue the files
923965 if ( isInitialized === false ) {
924966 console . log (
925- "Library not initialized, queuing files for later processing"
967+ "Library not initialized, queuing files for later processing" ,
926968 ) ;
927969 pendingFilesRef . current . push ( ...result . files ) ;
928970 return ;
929971 }
930972
931973 hasProcessedFilesRef . current = true ;
932974 toast . success (
933- t ( "fileHandler.filesReceived" , { count : result . successCount } )
975+ t ( "fileHandler.filesReceived" , { count : result . successCount } ) ,
934976 ) ;
935977 await importAudioFiles ( result . files , addSong , t ) ;
936978 // Clear processed files after successful import to allow re-importing the same files
937979 processedFilesRef . current . clear ( ) ;
938980 }
939981 if ( result . errorCount > 0 ) {
940982 toast . error (
941- t ( "fileHandler.filesSkipped" , { count : result . errorCount } )
983+ t ( "fileHandler.filesSkipped" , { count : result . errorCount } ) ,
942984 ) ;
943985 }
944986 } ) ;
@@ -959,13 +1001,13 @@ export function useFileHandler(
9591001 hasProcessedFilesRef . current = true ;
9601002 processedFilesRef . current . clear ( ) ;
9611003 toast . success (
962- t ( "fileHandler.filesReceived" , { count : filesToProcess . length } )
1004+ t ( "fileHandler.filesReceived" , { count : filesToProcess . length } ) ,
9631005 ) ;
9641006 } )
9651007 . catch ( ( error ) => {
9661008 console . error ( "Failed to process queued files:" , error ) ;
9671009 toast . error (
968- t ( "filePicker.failedImport" , { count : filesToProcess . length } )
1010+ t ( "filePicker.failedImport" , { count : filesToProcess . length } ) ,
9691011 ) ;
9701012 } ) ;
9711013 }
0 commit comments