import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from '~/store'; import { decryptionQueue } from '~/decrypt-worker/client'; import type { DecryptionResult } from '~/decrypt-worker/constants'; export enum ProcessState { UNTOUCHED = 'UNTOUCHED', COMPLETE = 'COMPLETE', ERROR = 'ERROR', } export enum ListingMode { LIST = 'LIST', CARD = 'CARD', } export interface AudioMetadata { name: string; artist: string; album: string; albumArtist: string; cover: string; // blob uri } export interface DecryptedAudioFile { fileName: string; raw: string; // blob uri ext: string; decrypted: string; // blob uri state: ProcessState; errorMessage: null | string; metadata: null | AudioMetadata; } export interface FileListingState { files: Record; displayMode: ListingMode; } const initialState: FileListingState = { files: Object.create(null), displayMode: ListingMode.LIST, }; export const processFile = createAsyncThunk< DecryptionResult, { fileId: string }, { rejectValue: { message: string; stack?: string } } >('fileListing/processFile', async ({ fileId }, thunkAPI) => { const file = selectFiles(thunkAPI.getState() as RootState)[fileId]; if (!file) { const { message, stack } = new Error('ERROR: File not found'); return thunkAPI.rejectWithValue({ message, stack }); } return decryptionQueue.add({ id: fileId, blobURI: file.raw }); }); export const fileListingSlice = createSlice({ name: 'fileListing', initialState, reducers: { addNewFile: (state, { payload }: PayloadAction<{ id: string; fileName: string; blobURI: string }>) => { state.files[payload.id] = { fileName: payload.fileName, raw: payload.blobURI, decrypted: '', ext: '', state: ProcessState.UNTOUCHED, errorMessage: null, metadata: null, }; }, setDecryptedContent: (state, { payload }: PayloadAction<{ id: string; decryptedBlobURI: string }>) => { const file = state.files[payload.id]; if (file) { file.decrypted = payload.decryptedBlobURI; } }, deleteFile: (state, { payload }: PayloadAction<{ id: string }>) => { if (state.files[payload.id]) { const file = state.files[payload.id]; if (file.decrypted) { URL.revokeObjectURL(file.decrypted); } if (file.raw) { URL.revokeObjectURL(file.raw); } delete state.files[payload.id]; } }, }, extraReducers(builder) { builder.addCase(processFile.fulfilled, (state, action) => { const { fileId } = action.meta.arg; const file = state.files[fileId]; if (!file) return; file.state = ProcessState.COMPLETE; file.decrypted = action.payload.decrypted; file.ext = action.payload.ext; // TODO: populate file metadata }); builder.addCase(processFile.rejected, (state, action) => { const { fileId } = action.meta.arg; const file = state.files[fileId]; if (!file) return; if (action.payload) { file.errorMessage = action.payload.message; } else { file.errorMessage = action.error.message || 'unknown error'; } }); }, }); export const { addNewFile, setDecryptedContent, deleteFile } = fileListingSlice.actions; export const selectFileCount = (state: RootState) => state.fileListing.files.length; export const selectFiles = (state: RootState) => state.fileListing.files; export const selectFileListingMode = (state: RootState) => state.fileListing.displayMode; export default fileListingSlice.reducer;