diff --git a/src/App/App.js b/src/App/App.js index 85697ddca..0e5bb3fe1 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -5,9 +5,9 @@ const React = require('react'); const { useTranslation } = require('react-i18next'); const { useCore } = require('stremio/core'); const { Router } = require('stremio-router'); -const { Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider, GamepadProvider } = require('stremio/services'); +const { Shell, Chromecast, KeyboardShortcuts, ServicesProvider, GamepadProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); -const { FileDropProvider, FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useShell, useBinaryState, useProfile, withCoreSuspender } = require('stremio/common'); +const { FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useShell, useBinaryState, useProfile, withCoreSuspender, onFileDrop } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); const DeepLinkHandler = require('./DeepLinkHandler'); const SearchParamsHandler = require('./SearchParamsHandler'); @@ -34,7 +34,6 @@ const App = () => { shell: new Shell(), chromecast: new Chromecast(), keyboardShortcuts: new KeyboardShortcuts(), - dragAndDrop: new DragAndDrop({ core }) }; }, []); const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false); @@ -51,6 +50,16 @@ const App = () => { } }, [toggleShortcutModal, toggleGamepadModal]); + onFileDrop(['application/x-bittorrent'], (file, buffer) => { + core.transport.dispatch({ + action: 'StreamingServer', + args: { + action: 'CreateTorrent', + args: Array.from(new Uint8Array(buffer)) + } + }); + }); + React.useEffect(() => { let prevPath = window.location.hash.slice(1); const onLocationHashChange = () => { @@ -82,13 +91,11 @@ const App = () => { services.shell.start(); services.chromecast.start(); services.keyboardShortcuts.start(); - services.dragAndDrop.start(); window.services = services; return () => { services.shell.stop(); services.chromecast.stop(); services.keyboardShortcuts.stop(); - services.dragAndDrop.stop(); services.chromecast.off('stateChanged', onChromecastStateChange); }; }, []); @@ -174,29 +181,27 @@ const App = () => { - - - - - { - shortcutModalOpen && - } - { - gamepadModalOpen && - } - - - - - - - - - + + + + { + shortcutModalOpen && + } + { + gamepadModalOpen && + } + + + + + + + + diff --git a/src/App/ServicesToaster.js b/src/App/ServicesToaster.js index 6338fba00..129a25131 100644 --- a/src/App/ServicesToaster.js +++ b/src/App/ServicesToaster.js @@ -1,14 +1,14 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); -const { useServices } = require('stremio/services'); const { useCore } = require('stremio/core'); -const { useToast } = require('stremio/common'); +const { useToast, useFileDrop } = require('stremio/common'); const ServicesToaster = () => { - const { dragAndDrop } = useServices(); const core = useCore(); const toast = useToast(); + const filedrop = useFileDrop(); + React.useEffect(() => { const onCoreEvent = (name, data) => { switch (name) { @@ -53,21 +53,23 @@ const ServicesToaster = () => { } }); }; - const onDragAndDropError = (error) => { - toast.show({ - type: 'error', - title: error.message, - message: error.file?.name, - timeout: 4000 - }); + const onFileDrop = (file, buffer, supported) => { + if (!supported) { + toast.show({ + type: 'error', + title: 'Unsupported file', + message: file.name, + timeout: 4000 + }); + } }; core.on('event', onCoreEvent); core.on('error', onCoreError); - dragAndDrop.on('error', onDragAndDropError); + filedrop.on('*', onFileDrop); return () => { core.off('event', onCoreEvent); core.off('error', onCoreError); - dragAndDrop.off('error', onDragAndDropError); + filedrop.off('*', onFileDrop); }; }, []); return null; diff --git a/src/App/styles.less b/src/App/styles.less index dc6aa512f..e12d4bbc0 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -213,22 +213,6 @@ html { transition: opacity 0.1s ease-out; } - .file-drop-container { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - border-radius: 1rem; - border: 0.5rem dashed transparent; - pointer-events: none; - transition: border-color 0.25s ease-out; - - &:global(.active) { - border-color: var(--primary-accent-color); - } - } - .updater-banner-container { z-index: 1; position: absolute; diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js index 104d24a44..85b2ada08 100644 --- a/src/common/CONSTANTS.js +++ b/src/common/CONSTANTS.js @@ -46,6 +46,7 @@ const ICON_FOR_TYPE = new Map([ const MIME_SIGNATURES = { 'application/x-subrip': ['310D0A', '310A'], 'text/vtt': ['574542565454'], + 'application/x-bittorrent': ['64'], }; const SUPPORTED_LOCAL_SUBTITLES = [ diff --git a/src/common/FileDrop/FileDrop.tsx b/src/common/FileDrop/FileDrop.tsx index 2993991e7..008adb6a5 100644 --- a/src/common/FileDrop/FileDrop.tsx +++ b/src/common/FileDrop/FileDrop.tsx @@ -1,9 +1,10 @@ -import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; -import { isFileType } from './utils'; +import { isFileType, isFileTypeSupported } from './utils'; +import styles from './styles.less'; export type FileType = string; -export type FileDropListener = (filename: string, buffer: ArrayBuffer) => void; +export type FileDropListener = (file: File, buffer: ArrayBuffer, supported: boolean) => void; type FileDropContext = { on: (type: FileType, listener: FileDropListener) => void, @@ -13,12 +14,11 @@ type FileDropContext = { const FileDropContext = createContext({} as FileDropContext); type Props = { - className: string, - children: JSX.Element, + children: React.ReactNode, }; -const FileDropProvider = ({ className, children }: Props) => { - const [listeners, setListeners] = useState<[FileType, FileDropListener][]>([]); +const FileDropProvider = ({ children }: Props) => { + const listeners = useRef<[FileType, FileDropListener][]>([]); const [active, setActive] = useState(false); const onDragOver = (event: DragEvent) => { @@ -30,38 +30,38 @@ const FileDropProvider = ({ className, children }: Props) => { setActive(false); }; - const onDrop = useCallback((event: DragEvent) => { - event.preventDefault(); - const { dataTransfer } = event; - - if (dataTransfer && dataTransfer?.files.length > 0) { - const file = dataTransfer.files[0]; - - file - .arrayBuffer() - .then((buffer) => { - listeners - .filter(([type]) => file.type ? type === file.type : isFileType(buffer, type)) - .forEach(([, listener]) => listener(file.name, buffer)); - }); - } - - setActive(false); - }, [listeners]); - const on = (type: FileType, listener: FileDropListener) => { - setListeners((listeners) => { - return [...listeners, [type, listener]]; - }); + listeners.current = [...listeners.current, [type, listener]]; }; const off = (type: FileType, listener: FileDropListener) => { - setListeners((listeners) => { - return listeners.filter(([key, value]) => key !== type && value !== listener); - }); + listeners.current = listeners.current.filter(([key, value]) => key !== type && value !== listener); }; useEffect(() => { + const onDrop = (event: DragEvent) => { + event.preventDefault(); + const { dataTransfer } = event; + + if (dataTransfer && dataTransfer?.files.length > 0) { + const file = dataTransfer.files[0]; + + file + .arrayBuffer() + .then((buffer) => { + listeners.current + .filter(([type]) => type === '*') + .forEach(([, listener]) => listener(file, buffer, isFileTypeSupported(buffer))); + listeners.current + .filter(([type]) => type !== '*' && (file.type ? type === file.type : isFileType(buffer, type))) + .forEach(([, listener]) => listener(file, buffer, true)); + }) + .catch(console.error); + } + + setActive(false); + }; + window.addEventListener('dragover', onDragOver); window.addEventListener('dragleave', onDragLeave); window.addEventListener('drop', onDrop); @@ -71,12 +71,12 @@ const FileDropProvider = ({ className, children }: Props) => { window.removeEventListener('dragleave', onDragLeave); window.removeEventListener('drop', onDrop); }; - }, [onDrop]); + }, []); return ( { children } -
+
); }; diff --git a/src/common/FileDrop/onFileDrop.ts b/src/common/FileDrop/onFileDrop.ts index 339b0219b..2eb9ea40a 100644 --- a/src/common/FileDrop/onFileDrop.ts +++ b/src/common/FileDrop/onFileDrop.ts @@ -6,7 +6,6 @@ const onFileDrop = (types: FileType[], listener: FileDropListener) => { useEffect(() => { types.forEach((type) => on(type, listener)); - return () => types.forEach((type) => off(type, listener)); }, []); }; diff --git a/src/common/FileDrop/styles.less b/src/common/FileDrop/styles.less new file mode 100644 index 000000000..6a538a4fe --- /dev/null +++ b/src/common/FileDrop/styles.less @@ -0,0 +1,15 @@ +.file-drop-container { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + border-radius: 1rem; + border: 0.5rem dashed transparent; + pointer-events: none; + transition: border-color 0.25s ease-out; + + &:global(.active) { + border-color: var(--primary-accent-color); + } +} \ No newline at end of file diff --git a/src/common/FileDrop/utils.ts b/src/common/FileDrop/utils.ts index f9996aed3..2fa69bbf7 100644 --- a/src/common/FileDrop/utils.ts +++ b/src/common/FileDrop/utils.ts @@ -14,6 +14,11 @@ const isFileType = (buffer: ArrayBuffer, type: string) => { }); }; +const isFileTypeSupported = (buffer: ArrayBuffer) => { + return Object.keys(SIGNATURES).some((type) => isFileType(buffer, type)); +}; + export { isFileType, + isFileTypeSupported, }; diff --git a/src/common/index.js b/src/common/index.js index 1963e8995..c27b37178 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -1,6 +1,6 @@ // Copyright (C) 2017-2023 Smart code 203358507 -const { FileDropProvider, onFileDrop } = require('./FileDrop'); +const { FileDropProvider, useFileDrop, onFileDrop } = require('./FileDrop'); const { FullscreenProvider, useFullscreen } = require('./Fullscreen'); const { PlatformProvider, usePlatform } = require('./Platform'); const { ToastProvider, useToast } = require('./Toast'); @@ -33,6 +33,7 @@ const { default: useLanguageSorting } = require('./useLanguageSorting'); module.exports = { FileDropProvider, + useFileDrop, onFileDrop, FullscreenProvider, PlatformProvider, diff --git a/src/index.js b/src/index.js index e9061296c..74d39601d 100755 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ const { initReactI18next } = require('react-i18next'); const stremioTranslations = require('stremio-translations'); const App = require('./App'); const { CoreProvider } = require('./core'); +const { FileDropProvider } = require('./common'); const translations = Object.fromEntries(Object.entries(stremioTranslations()).map(([key, value]) => [key, { translation: value @@ -42,7 +43,9 @@ const appInfo = { const root = ReactDOM.createRoot(document.getElementById('app')); root.render( - + + + ); diff --git a/src/routes/Player/useSubtitles.ts b/src/routes/Player/useSubtitles.ts index 09fb6be78..26de76d74 100644 --- a/src/routes/Player/useSubtitles.ts +++ b/src/routes/Player/useSubtitles.ts @@ -151,8 +151,8 @@ const useSubtitles = ({ streamStateChanged({ subtitleOffset: offset }); }, [streamStateChanged, video]); - onFileDrop(CONSTANTS.SUPPORTED_LOCAL_SUBTITLES, (filename: string, buffer: ArrayBuffer) => { - videoRef.current.addLocalSubtitles(filename, buffer); + onFileDrop(CONSTANTS.SUPPORTED_LOCAL_SUBTITLES, (file: File, buffer: ArrayBuffer) => { + videoRef.current.addLocalSubtitles(file.name, buffer); }); useEffect(() => { diff --git a/src/services/DragAndDrop/DragAndDrop.js b/src/services/DragAndDrop/DragAndDrop.js deleted file mode 100644 index 1538c328e..000000000 --- a/src/services/DragAndDrop/DragAndDrop.js +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const EventEmitter = require('eventemitter3'); - -function DragAndDrop({ core }) { - let active = false; - - const events = new EventEmitter(); - - function onDragOver(event) { - event.preventDefault(); - } - async function onDrop(event) { - event.preventDefault(); - if (event.dataTransfer.files instanceof FileList && event.dataTransfer.files.length > 0) { - const file = event.dataTransfer.files[0]; - switch (file.type) { - case 'application/x-bittorrent': { - try { - const torrent = await file.arrayBuffer(); - core.transport.dispatch({ - action: 'StreamingServer', - args: { - action: 'CreateTorrent', - args: Array.from(new Uint8Array(torrent)) - } - }); - } catch (_error) { - events.emit('error', { - message: 'Failed to process file', - file: { - name: file.name, - type: file.type - } - }); - } - break; - } - case 'application/x-subrip': - break; - case 'text/vtt': - break; - case '': - break; - default: { - events.emit('error', { - message: 'Unsupported file', - file: { - name: file.name, - type: file.type - } - }); - } - } - } - } - function onStateChanged() { - events.emit('stateChanged'); - } - - Object.defineProperties(this, { - active: { - configurable: false, - enumerable: true, - get: function() { - return active; - } - } - }); - - this.start = function() { - if (active) { - return; - } - - window.addEventListener('dragover', onDragOver); - window.addEventListener('drop', onDrop); - active = true; - onStateChanged(); - }; - this.stop = function() { - window.removeEventListener('dragover', onDragOver); - window.removeEventListener('drop', onDrop); - active = false; - onStateChanged(); - }; - this.on = function(name, listener) { - events.on(name, listener); - }; - this.off = function(name, listener) { - events.off(name, listener); - }; -} - -module.exports = DragAndDrop; diff --git a/src/services/DragAndDrop/index.js b/src/services/DragAndDrop/index.js deleted file mode 100644 index 5fb7baf15..000000000 --- a/src/services/DragAndDrop/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const DragAndDrop = require('./DragAndDrop'); - -module.exports = DragAndDrop; diff --git a/src/services/ServicesContext/types.d.ts b/src/services/ServicesContext/types.d.ts index 45dde7ed9..49b80525b 100644 --- a/src/services/ServicesContext/types.d.ts +++ b/src/services/ServicesContext/types.d.ts @@ -2,5 +2,4 @@ type ServicesContext = { shell: any, chromecast: any, keyboardShortcuts: any, - dragAndDrop: any, }; diff --git a/src/services/index.js b/src/services/index.js index 17f00719d..aede80a89 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -1,7 +1,6 @@ // Copyright (C) 2017-2023 Smart code 203358507 const Chromecast = require('./Chromecast'); -const DragAndDrop = require('./DragAndDrop'); const KeyboardShortcuts = require('./KeyboardShortcuts'); const { ServicesProvider, useServices } = require('./ServicesContext'); const { GamepadProvider, useGamepad } = require('./GamepadContext'); @@ -9,7 +8,6 @@ const Shell = require('./Shell'); module.exports = { Chromecast, - DragAndDrop, KeyboardShortcuts, ServicesProvider, useServices,