Skip to content

Commit c6905af

Browse files
committed
It works!!!
1 parent c3eb3c9 commit c6905af

5 files changed

Lines changed: 302 additions & 231 deletions

File tree

Build/src/helpers/filePickerHelper.tsx

Lines changed: 75 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
7272
async 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

718718
export 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+
811837
export 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
840866
export 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(
899941
export 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
}

Build/src/helpers/importAudioFiles.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { setAlbumArtInCache } from "../hooks/useAlbumArt";
66
export async function importAudioFiles(
77
audioFiles: Array<{ file: File } | File>,
88
addSong: (song: Song, file: File) => Promise<void>,
9-
t: any
9+
t: any,
1010
) {
1111
if (!audioFiles || audioFiles.length === 0) return;
1212

@@ -39,7 +39,7 @@ export async function importAudioFiles(
3939
try {
4040
const arrayBuffer = await file.arrayBuffer();
4141
const isSafari = /^((?!chrome|android).)*safari/i.test(
42-
navigator.userAgent
42+
navigator.userAgent,
4343
);
4444

4545
if (isSafari) {
@@ -50,7 +50,7 @@ export async function importAudioFiles(
5050
processedFile = new File(
5151
[wavBlob],
5252
file.name.replace(/\.flo$/i, ".wav"),
53-
{ type: "audio/wav" }
53+
{ type: "audio/wav" },
5454
);
5555
processedMimeType = "audio/wav";
5656
console.log(`Pre-decoded flo to WAV for Safari: ${file.name}`);
@@ -60,7 +60,7 @@ export async function importAudioFiles(
6060
const audioContext = new AudioContext();
6161
const audioBuffer = await decodeFloToAudioBuffer(
6262
arrayBuffer,
63-
audioContext
63+
audioContext,
6464
);
6565

6666
// Store as interleaved Float32Array PCM
@@ -80,7 +80,7 @@ export async function importAudioFiles(
8080
processedFile = new File(
8181
[pcmBlob],
8282
file.name.replace(/\.flo$/i, ".pcm"),
83-
{ type: "audio/pcm" }
83+
{ type: "audio/pcm" },
8484
);
8585
processedMimeType = "audio/pcm";
8686

@@ -101,7 +101,7 @@ export async function importAudioFiles(
101101
} catch (error) {
102102
console.warn(
103103
"Failed to pre-decode flo file, storing original:",
104-
error
104+
error,
105105
);
106106
// Keep original file if pre-decoding fails
107107
processedMimeType = "audio/x-flo";

0 commit comments

Comments
 (0)