Skip to content

Commit b3502eb

Browse files
committed
Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/hardware-decoding
2 parents e2b2286 + 5ac55f6 commit b3502eb

22 files changed

+374
-33
lines changed

package-lock.json

+7-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@stremio/stremio-colors": "5.2.0",
1919
"@stremio/stremio-core-web": "0.48.5",
2020
"@stremio/stremio-icons": "5.4.1",
21-
"@stremio/stremio-video": "github:Stremio/stremio-video#b5e6eb7c3aa85703c3468c53e489d7ec47daccae",
21+
"@stremio/stremio-video": "0.0.53",
2222
"a-color-picker": "1.2.1",
2323
"bowser": "2.11.0",
2424
"buffer": "6.0.3",
@@ -40,7 +40,7 @@
4040
"react-i18next": "^15.1.3",
4141
"react-is": "18.3.1",
4242
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
43-
"stremio-translations": "github:Stremio/stremio-translations#a0f50634202f748a57907b645d2cd92fbaa479dd",
43+
"stremio-translations": "github:Stremio/stremio-translations#62bcc6e8f44258203c7375af59210771efb6f634",
4444
"url": "0.11.4",
4545
"use-long-press": "^3.2.0"
4646
},

src/App/App.js

+13-9
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ const { useTranslation } = require('react-i18next');
66
const { Router } = require('stremio-router');
77
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
88
const { NotFound } = require('stremio/routes');
9-
const { PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
9+
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
1010
const ServicesToaster = require('./ServicesToaster');
1111
const DeepLinkHandler = require('./DeepLinkHandler');
1212
const SearchParamsHandler = require('./SearchParamsHandler');
13+
const { default: UpdaterBanner } = require('./UpdaterBanner');
1314
const ErrorDialog = require('./ErrorDialog');
1415
const withProtectedRoutes = require('./withProtectedRoutes');
1516
const routerViewsConfig = require('./routerViewsConfig');
@@ -165,14 +166,17 @@ const App = () => {
165166
<PlatformProvider>
166167
<ToastProvider className={styles['toasts-container']}>
167168
<TooltipProvider className={styles['tooltip-container']}>
168-
<ServicesToaster />
169-
<DeepLinkHandler />
170-
<SearchParamsHandler />
171-
<RouterWithProtectedRoutes
172-
className={styles['router']}
173-
viewsConfig={routerViewsConfig}
174-
onPathNotMatch={onPathNotMatch}
175-
/>
169+
<FileDropProvider className={styles['file-drop-container']}>
170+
<ServicesToaster />
171+
<DeepLinkHandler />
172+
<SearchParamsHandler />
173+
<UpdaterBanner className={styles['updater-banner-container']} />
174+
<RouterWithProtectedRoutes
175+
className={styles['router']}
176+
viewsConfig={routerViewsConfig}
177+
onPathNotMatch={onPathNotMatch}
178+
/>
179+
</FileDropProvider>
176180
</TooltipProvider>
177181
</ToastProvider>
178182
</PlatformProvider>
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
.updater-banner {
2+
height: 4rem;
3+
display: flex;
4+
align-items: center;
5+
justify-content: center;
6+
gap: 1rem;
7+
padding: 0 1rem;
8+
font-size: 1rem;
9+
font-weight: bold;
10+
color: var(--primary-foreground-color);
11+
background-color: var(--primary-accent-color);
12+
13+
.button {
14+
display: flex;
15+
flex-direction: row;
16+
align-items: center;
17+
justify-content: center;
18+
height: 2.5rem;
19+
padding: 0 1rem;
20+
border-radius: var(--border-radius);
21+
color: var(--primary-background-color);
22+
background-color: var(--primary-foreground-color);
23+
transition: all 0.1s ease-out;
24+
25+
&:hover {
26+
color: var(--primary-foreground-color);
27+
background-color: transparent;
28+
box-shadow: inset 0 0 0 0.15rem var(--primary-foreground-color);
29+
}
30+
}
31+
32+
.close {
33+
position: absolute;
34+
right: 0;
35+
height: 4rem;
36+
width: 4rem;
37+
display: flex;
38+
flex-direction: row;
39+
align-items: center;
40+
justify-content: center;
41+
42+
.icon {
43+
height: 2rem;
44+
}
45+
}
46+
}
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, { useEffect } from 'react';
2+
import Icon from '@stremio/stremio-icons/react';
3+
import { useTranslation } from 'react-i18next';
4+
import { useServices } from 'stremio/services';
5+
import { useBinaryState, useShell } from 'stremio/common';
6+
import { Button, Transition } from 'stremio/components';
7+
import styles from './UpdaterBanner.less';
8+
9+
type Props = {
10+
className: string,
11+
};
12+
13+
const UpdaterBanner = ({ className }: Props) => {
14+
const { t } = useTranslation();
15+
const { shell } = useServices();
16+
const shellTransport = useShell();
17+
const [visible, show, hide] = useBinaryState(false);
18+
19+
const onInstallClick = () => {
20+
shellTransport.send('autoupdater-notif-clicked');
21+
};
22+
23+
useEffect(() => {
24+
shell.transport && shell.transport.on('autoupdater-show-notif', show);
25+
26+
return () => {
27+
shell.transport && shell.transport.off('autoupdater-show-notif', show);
28+
};
29+
}, []);
30+
31+
return (
32+
<div className={className}>
33+
<Transition when={visible} name={'slide-up'}>
34+
<div className={styles['updater-banner']}>
35+
<div className={styles['label']}>
36+
{ t('UPDATER_TITLE') }
37+
</div>
38+
<Button className={styles['button']} onClick={onInstallClick}>
39+
{ t('UPDATER_INSTALL_BUTTON') }
40+
</Button>
41+
<Button className={styles['close']} onClick={hide}>
42+
<Icon className={styles['icon']} name={'close'} />
43+
</Button>
44+
</div>
45+
</Transition>
46+
</div>
47+
);
48+
};
49+
50+
export default UpdaterBanner;

src/App/UpdaterBanner/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import UpdaterBanner from './UpdaterBanner';
2+
export default UpdaterBanner;

src/App/styles.less

+21-1
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,32 @@ html {
202202
background-color: var(--modal-background-color);
203203
box-shadow: var(--outer-glow);
204204
transition: opacity 0.1s ease-out;
205+
}
206+
207+
.file-drop-container {
208+
position: fixed;
209+
top: 0;
210+
bottom: 0;
211+
left: 0;
212+
right: 0;
213+
border-radius: 1rem;
214+
border: 0.5rem dashed transparent;
215+
pointer-events: none;
216+
transition: border-color 0.25s ease-out;
205217

206218
&:global(.active) {
207-
transition-delay: 0.25s;
219+
border-color: var(--primary-accent-color);
208220
}
209221
}
210222

223+
.updater-banner-container {
224+
z-index: 1;
225+
position: absolute;
226+
left: 0;
227+
right: 0;
228+
bottom: 0;
229+
}
230+
211231
.router {
212232
width: 100%;
213233
height: 100%;

src/common/CONSTANTS.js

+12
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ const ICON_FOR_TYPE = new Map([
4141
['other', 'movies'],
4242
]);
4343

44+
const MIME_SIGNATURES = {
45+
'application/x-subrip': ['310D0A', '310A'],
46+
'text/vtt': ['574542565454'],
47+
};
48+
49+
const SUPPORTED_LOCAL_SUBTITLES = [
50+
'application/x-subrip',
51+
'text/vtt',
52+
];
53+
4454
const EXTERNAL_PLAYERS = [
4555
{
4656
label: 'EXTERNAL_PLAYER_DISABLED',
@@ -113,6 +123,8 @@ module.exports = {
113123
WRITERS_LINK_CATEGORY,
114124
TYPE_PRIORITIES,
115125
ICON_FOR_TYPE,
126+
MIME_SIGNATURES,
127+
SUPPORTED_LOCAL_SUBTITLES,
116128
EXTERNAL_PLAYERS,
117129
WHITELISTED_HOSTS,
118130
};

src/common/FileDrop/FileDrop.tsx

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
2+
import classNames from 'classnames';
3+
import { isFileType } from './utils';
4+
5+
export type FileType = string;
6+
export type FileDropListener = (filename: string, buffer: ArrayBuffer) => void;
7+
8+
type FileDropContext = {
9+
on: (type: FileType, listener: FileDropListener) => void,
10+
off: (type: FileType, listener: FileDropListener) => void,
11+
};
12+
13+
const FileDropContext = createContext({} as FileDropContext);
14+
15+
type Props = {
16+
className: string,
17+
children: JSX.Element,
18+
};
19+
20+
const FileDropProvider = ({ className, children }: Props) => {
21+
const [listeners, setListeners] = useState<[FileType, FileDropListener][]>([]);
22+
const [active, setActive] = useState(false);
23+
24+
const onDragOver = (event: DragEvent) => {
25+
event.preventDefault();
26+
setActive(true);
27+
};
28+
29+
const onDragLeave = () => {
30+
setActive(false);
31+
};
32+
33+
const onDrop = useCallback((event: DragEvent) => {
34+
event.preventDefault();
35+
const { dataTransfer } = event;
36+
37+
if (dataTransfer && dataTransfer?.files.length > 0) {
38+
const file = dataTransfer.files[0];
39+
40+
file
41+
.arrayBuffer()
42+
.then((buffer) => {
43+
listeners
44+
.filter(([type]) => file.type ? type === file.type : isFileType(buffer, type))
45+
.forEach(([, listerner]) => listerner(file.name, buffer));
46+
});
47+
}
48+
49+
setActive(false);
50+
}, [listeners]);
51+
52+
const on = (type: FileType, listener: FileDropListener) => {
53+
setListeners((listeners) => {
54+
return [...listeners, [type, listener]];
55+
});
56+
};
57+
58+
const off = (type: FileType, listener: FileDropListener) => {
59+
setListeners((listeners) => {
60+
return listeners.filter(([key, value]) => key !== type && value !== listener);
61+
});
62+
};
63+
64+
useEffect(() => {
65+
window.addEventListener('dragover', onDragOver);
66+
window.addEventListener('dragleave', onDragLeave);
67+
window.addEventListener('drop', onDrop);
68+
69+
return () => {
70+
window.removeEventListener('dragover', onDragOver);
71+
window.removeEventListener('dragleave', onDragLeave);
72+
window.removeEventListener('drop', onDrop);
73+
};
74+
}, [onDrop]);
75+
76+
return (
77+
<FileDropContext.Provider value={{ on, off }}>
78+
{ children }
79+
<div className={classNames(className, { 'active': active })} />
80+
</FileDropContext.Provider>
81+
);
82+
};
83+
84+
const useFileDrop = () => {
85+
return useContext(FileDropContext);
86+
};
87+
88+
export {
89+
FileDropProvider,
90+
useFileDrop,
91+
};

src/common/FileDrop/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { FileDropProvider, useFileDrop } from './FileDrop';
2+
import onFileDrop from './onFileDrop';
3+
4+
export {
5+
FileDropProvider,
6+
useFileDrop,
7+
onFileDrop,
8+
};

src/common/FileDrop/onFileDrop.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useEffect } from 'react';
2+
import { type FileType, type FileDropListener, useFileDrop } from './FileDrop';
3+
4+
const onFileDrop = (types: FileType[], listener: FileDropListener) => {
5+
const { on, off } = useFileDrop();
6+
7+
useEffect(() => {
8+
types.forEach((type) => on(type, listener));
9+
10+
return () => types.forEach((type) => off(type, listener));
11+
}, []);
12+
};
13+
14+
export default onFileDrop;

src/common/FileDrop/utils.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { MIME_SIGNATURES } from 'stremio/common/CONSTANTS';
2+
3+
const SIGNATURES = MIME_SIGNATURES as Record<string, string[]>;
4+
5+
const isFileType = (buffer: ArrayBuffer, type: string) => {
6+
const signatures = SIGNATURES[type];
7+
8+
return signatures.some((signature) => {
9+
const array = new Uint8Array(buffer);
10+
const signatureBuffer = Buffer.from(signature, 'hex');
11+
const bufferToCompare = array.subarray(0, signatureBuffer.length);
12+
13+
return Buffer.compare(signatureBuffer, bufferToCompare) === 0;
14+
});
15+
};
16+
17+
export {
18+
isFileType,
19+
};

0 commit comments

Comments
 (0)