diff --git a/client/src/javascript/components/AppWrapper.tsx b/client/src/javascript/components/AppWrapper.tsx index fd0404a35..fa4912a2b 100644 --- a/client/src/javascript/components/AppWrapper.tsx +++ b/client/src/javascript/components/AppWrapper.tsx @@ -12,6 +12,7 @@ import AuthStore from '@client/stores/AuthStore'; import ConfigStore from '@client/stores/ConfigStore'; import ClientStatusStore from '@client/stores/ClientStatusStore'; import UIStore from '@client/stores/UIStore'; +import { processFiles } from '@client/util/fileProcessor' import ClientConnectionInterruption from './general/ClientConnectionInterruption'; import WindowTitle from './general/WindowTitle'; @@ -23,6 +24,16 @@ interface AppWrapperProps { className?: string; } +declare global { + interface Window { + launchQueue: { + setConsumer(consumer: (launchParams: { + files: FileSystemFileHandle[] + }) => any): void; + }; + } +} + const AppWrapper: FC = observer(({children, className}: AppWrapperProps) => { const navigate = useNavigate(); @@ -55,6 +66,17 @@ const AppWrapper: FC = observer(({children, className}: AppWrap } } + if ('launchQueue' in window) { + window.launchQueue.setConsumer(async (launchParams) => { + if (launchParams.files && launchParams.files.length) { + const processedFiles = await processFiles(launchParams.files); + if (processedFiles.length) { + UIStore.setActiveModal({ id: 'add-torrents', tab: 'by-file', files: processedFiles }); + } + } + }); + } + let overlay: ReactNode = null; if (!AuthStore.isAuthenticating || (AuthStore.isAuthenticated && !UIStore.haveUIDependenciesResolved)) { overlay = ; diff --git a/client/src/javascript/components/general/form-elements/FileDropzone.tsx b/client/src/javascript/components/general/form-elements/FileDropzone.tsx index 3f72e7fc6..c8cc38da0 100644 --- a/client/src/javascript/components/general/form-elements/FileDropzone.tsx +++ b/client/src/javascript/components/general/form-elements/FileDropzone.tsx @@ -5,7 +5,8 @@ import {Trans} from '@lingui/react'; import {Close, File, Files} from '@client/ui/icons'; import {FormRowItem} from '@client/ui'; -export type ProcessedFiles = Array<{name: string; data: string}>; +import type { ProcessedFiles } from '@client/util/fileProcessor'; +import { processFiles } from '@client/util/fileProcessor' interface FileDropzoneProps { initialFiles?: ProcessedFiles; @@ -55,23 +56,11 @@ const FileDropzone: FC = ({initialFiles, onFilesChanged}: Fil ) : null} ) => { - const processedFiles: ProcessedFiles = []; - addedFiles.forEach((file) => { - const reader = new FileReader(); - reader.onload = (e) => { - if (e.target?.result != null && typeof e.target.result === 'string') { - processedFiles.push({ - name: file.name, - data: e.target.result.split('base64,')[1], - }); - } - if (processedFiles.length === addedFiles.length) { - setFiles(files.concat(processedFiles)); - } - }; - reader.readAsDataURL(file); - }); + onDrop={async (addedFiles: Array) => { + const processedFiles = await processFiles(addedFiles); + if (processedFiles.length) { + setFiles(files.concat(processedFiles)); + } }} > {({getRootProps, getInputProps, isDragActive}) => ( diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx index d07b9054a..ca47b4c65 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx @@ -12,7 +12,7 @@ import FileDropzone from '../../general/form-elements/FileDropzone'; import FilesystemBrowserTextbox from '../../general/form-elements/FilesystemBrowserTextbox'; import TagSelect from '../../general/form-elements/TagSelect'; -import type {ProcessedFiles} from '../../general/form-elements/FileDropzone'; +import type {ProcessedFiles} from '@client/util/fileProcessor'; interface AddTorrentsByFileFormData { destination: string; diff --git a/client/src/javascript/components/torrent-list/TorrentListDropzone.tsx b/client/src/javascript/components/torrent-list/TorrentListDropzone.tsx index b7aa48356..1868a181b 100644 --- a/client/src/javascript/components/torrent-list/TorrentListDropzone.tsx +++ b/client/src/javascript/components/torrent-list/TorrentListDropzone.tsx @@ -3,27 +3,13 @@ import {useDropzone} from 'react-dropzone'; import UIStore from '@client/stores/UIStore'; -import type {ProcessedFiles} from '@client/components/general/form-elements/FileDropzone'; +import { processFiles } from '@client/util/fileProcessor'; -const handleFileDrop = (files: Array) => { - const processedFiles: ProcessedFiles = []; - - files.forEach((file) => { - const reader = new FileReader(); - reader.onload = (e) => { - if (e.target?.result != null && typeof e.target.result === 'string') { - processedFiles.push({ - name: file.name, - data: e.target.result.split('base64,')[1], - }); - } - - if (processedFiles.length === files.length && processedFiles[0] != null) { - UIStore.setActiveModal({id: 'add-torrents', tab: 'by-file', files: processedFiles}); - } - }; - reader.readAsDataURL(file); - }); +const handleFileDrop = async (files: Array) => { + const processedFiles = await processFiles(files); + if (processedFiles.length) { + UIStore.setActiveModal({ id: 'add-torrents', tab: 'by-file', files: processedFiles }); + } }; const TorrentListDropzone: FC<{children: ReactNode}> = ({children}: {children: ReactNode}) => { diff --git a/client/src/javascript/util/fileProcessor.ts b/client/src/javascript/util/fileProcessor.ts new file mode 100644 index 000000000..5d9fc14c0 --- /dev/null +++ b/client/src/javascript/util/fileProcessor.ts @@ -0,0 +1,36 @@ +interface ProcessedFile { name: string; data: string } + +function processFile(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener('loadend', ({ target }) => { + const result = target?.result as string; + if (result && typeof result === 'string') { + resolve({ + name: file?.name, + data: result.split('base64,')[1] + }); + } else { + reject(new Error(`Error reading file: ${file?.name}`)); + } + }); + reader.readAsDataURL(file); + }); +} + +export type ProcessedFiles = Array; +export async function processFiles(fileList: File[] | FileSystemFileHandle[]): Promise { + const processedFiles = []; + for (let file of fileList) { + try { + if (file instanceof FileSystemFileHandle) { + file = await file.getFile(); + } + const processedFile = await processFile(file); + processedFiles.push(processedFile); + } catch(error) { + console.error(error); + } + } + return processedFiles; +} \ No newline at end of file diff --git a/client/src/public/icon_144x144.png b/client/src/public/icon_144x144.png new file mode 100644 index 000000000..d099f9940 Binary files /dev/null and b/client/src/public/icon_144x144.png differ diff --git a/client/src/public/manifest.json b/client/src/public/manifest.json index c31b0f31d..73f2b6e36 100644 --- a/client/src/public/manifest.json +++ b/client/src/public/manifest.json @@ -40,7 +40,33 @@ "name": "Flood", "short_name": "Flood", "display": "standalone", - "start_url": "./index.html", + "protocol_handlers": [ + { + "protocol": "magnet", + "url": "/?action=add-urls&url=%s" + } + ], + "file_handlers": [ + { + "name": "Torrent", + "action": "/?action=add-files", + "accept": { + "application/x-bittorrent": ".torrent" + }, + "icons": [ + { + "src": "icon_144x144.png", + "sizes": "144x144", + "type": "image/png" + } + ], + "launch_type": "single-client" + } + ], + "launch_handler": { + "client_mode": "navigate-existing" + }, + "start_url": "/", "description": "Web UI for torrent clients", "background_color": "#ffffff", "theme_color": "#349cf4"