Merge pull request #1261 from Stremio/refactor/file-drop-logic
Some checks are pending
Build / build (push) Waiting to run

Dev: Move file drop logic to provider
This commit is contained in:
Tim 2026-05-06 09:14:20 +02:00 committed by GitHub
commit 5f0ecd2a3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 110 additions and 198 deletions

View file

@ -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 = () => {
<PlatformProvider>
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<FileDropProvider className={styles['file-drop-container']}>
<GamepadProvider enabled={gamepadSupportEnabled} onGuide={toggleGamepadModal}>
<ShortcutsProvider onShortcut={onShortcut}>
<FullscreenProvider>
{
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
}
{
gamepadModalOpen && <GamepadModal onClose={closeGamepadModal}/>
}
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</FullscreenProvider>
</ShortcutsProvider>
</GamepadProvider>
</FileDropProvider>
<GamepadProvider enabled={gamepadSupportEnabled} onGuide={toggleGamepadModal}>
<ShortcutsProvider onShortcut={onShortcut}>
<FullscreenProvider>
{
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
}
{
gamepadModalOpen && <GamepadModal onClose={closeGamepadModal}/>
}
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</FullscreenProvider>
</ShortcutsProvider>
</GamepadProvider>
</TooltipProvider>
</ToastProvider>
</PlatformProvider>

View file

@ -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;

View file

@ -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;

View file

@ -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 = [

View file

@ -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 (
<FileDropContext.Provider value={{ on, off }}>
{ children }
<div className={classNames(className, { 'active': active })} />
<div className={classNames(styles['file-drop-container'], { 'active': active })} />
</FileDropContext.Provider>
);
};

View file

@ -6,7 +6,6 @@ const onFileDrop = (types: FileType[], listener: FileDropListener) => {
useEffect(() => {
types.forEach((type) => on(type, listener));
return () => types.forEach((type) => off(type, listener));
}, []);
};

View file

@ -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);
}
}

View file

@ -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,
};

View file

@ -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,

View file

@ -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(
<CoreProvider appInfo={appInfo}>
<App />
<FileDropProvider>
<App />
</FileDropProvider>
</CoreProvider>
);

View file

@ -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(() => {

View file

@ -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;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const DragAndDrop = require('./DragAndDrop');
module.exports = DragAndDrop;

View file

@ -2,5 +2,4 @@ type ServicesContext = {
shell: any,
chromecast: any,
keyboardShortcuts: any,
dragAndDrop: any,
};

View file

@ -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,