Skip to content

Commit

Permalink
Merge pull request #690 from Stremio/feat/open-external-shell
Browse files Browse the repository at this point in the history
feat: impl openExternal for shell compatibility
  • Loading branch information
core1024 authored Sep 27, 2024
2 parents 7843ea5 + 02b9b5b commit 024df8e
Show file tree
Hide file tree
Showing 18 changed files with 176 additions and 75 deletions.
9 changes: 4 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"react-i18next": "^12.1.1",
"react-is": "18.2.0",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "github:Stremio/stremio-translations#b13b3e2653bd0dcf644d2a20ffa32074fe6532dd",
"stremio-translations": "github:Stremio/stremio-translations#378218c9617f3e763ba5f6755e4d39c1c158747d",
"url": "0.11.0",
"use-long-press": "^3.1.5"
},
Expand Down
28 changes: 15 additions & 13 deletions src/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next');
const { Router } = require('stremio-router');
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
const { ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
const { PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
const DeepLinkHandler = require('./DeepLinkHandler');
const SearchParamsHandler = require('./SearchParamsHandler');
Expand Down Expand Up @@ -162,18 +162,20 @@ const App = () => {
services.core.error instanceof Error ?
<ErrorDialog className={styles['error-container']} />
:
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</TooltipProvider>
</ToastProvider>
<PlatformProvider>
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</TooltipProvider>
</ToastProvider>
</PlatformProvider>
:
<div className={styles['loader-container']} />
}
Expand Down
5 changes: 3 additions & 2 deletions src/common/AddonDetailsModal/AddonDetailsModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const React = require('react');
const PropTypes = require('prop-types');
const ModalDialog = require('stremio/common/ModalDialog');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const { usePlatform, withCoreSuspender } = require('stremio/common/CoreSuspender');
const { useServices } = require('stremio/services');
const AddonDetailsWithRemoteAndLocalAddon = withRemoteAndLocalAddon(require('./AddonDetails'));
const useAddonDetails = require('./useAddonDetails');
Expand Down Expand Up @@ -43,6 +43,7 @@ function withRemoteAndLocalAddon(AddonDetails) {

const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
const { core } = useServices();
const platform = usePlatform();
const addonDetails = useAddonDetails(transportUrl);
const modalButtons = React.useMemo(() => {
const cancelButton = {
Expand All @@ -68,7 +69,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
label: 'Configure',
props: {
onClick: (event) => {
window.open(transportUrl.replace('manifest.json', 'configure'));
platform.openExternal(transportUrl.replace('manifest.json', 'configure'));
if (typeof onCloseRequest === 'function') {
onCloseRequest({
type: 'configure',
Expand Down
3 changes: 3 additions & 0 deletions src/common/CONSTANTS.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ const EXTERNAL_PLAYERS = [
},
];

const WHITELISTED_HOSTS = ['stremio.com', 'strem.io', 'stremio.zendesk.com', 'google.com', 'youtube.com', 'twitch.tv', 'twitter.com', 'netflix.com', 'adex.network', 'amazon.com', 'forms.gle'];

module.exports = {
CHROMECAST_RECEIVER_APP_ID,
SUBTITLES_SIZES,
Expand All @@ -110,4 +112,5 @@ module.exports = {
TYPE_PRIORITIES,
ICON_FOR_TYPE,
EXTERNAL_PLAYERS,
WHITELISTED_HOSTS,
};
51 changes: 51 additions & 0 deletions src/common/Platform/Platform.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { createContext, useContext } from 'react';
import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS';
import useShell from './useShell';
import { name, isMobile } from './device';

interface PlatformContext {
name: string;
isMobile: boolean;
openExternal: (url: string) => void;
}

const PlatformContext = createContext<PlatformContext | null>(null);

type Props = {
children: JSX.Element;
};

const PlatformProvider = ({ children }: Props) => {
const shell = useShell();

const openExternal = (url: string) => {
try {
const { hostname } = new URL(url);
const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname.endsWith(host));
const finalUrl = !isWhitelisted ? `https://www.stremio.com/warning#${encodeURIComponent(url)}` : url;

if (shell.active) {
shell.send('open-external', finalUrl);
} else {
window.open(finalUrl, '_blank');
}
} catch (e) {
console.error('Failed to parse external url:', e);
}
};

return (
<PlatformContext.Provider value={{ openExternal, name, isMobile }}>
{children}
</PlatformContext.Provider>
);
};

const usePlatform = () => {
return useContext(PlatformContext);
};

export {
PlatformProvider,
usePlatform
};
31 changes: 31 additions & 0 deletions src/common/Platform/device.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Bowser from 'bowser';

const APPLE_MOBILE_DEVICES = [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod',
];

const { userAgent, platform, maxTouchPoints } = globalThis.navigator;

// this detects ipad properly in safari
// while bowser does not
const isIOS = APPLE_MOBILE_DEVICES.includes(platform) || (userAgent.includes('Mac') && 'ontouchend' in document);

// Edge case: iPad is included in this function
// Keep in mind maxTouchPoints for Vision Pro might change in the future
const isVisionOS = userAgent.includes('Macintosh') || maxTouchPoints === 5;

const bowser = Bowser.getParser(userAgent);
const os = bowser.getOSName().toLowerCase();

const name = isVisionOS ? 'visionos' : isIOS ? 'ios' : os || 'unknown';
const isMobile = ['ios', 'android'].includes(name);

export {
name,
isMobile,
};
5 changes: 5 additions & 0 deletions src/common/Platform/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PlatformProvider, usePlatform } from './Platform';
export {
PlatformProvider,
usePlatform,
};
22 changes: 22 additions & 0 deletions src/common/Platform/useShell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const createId = () => Math.floor(Math.random() * 9999) + 1;

const useShell = () => {
const transport = globalThis?.qt?.webChannelTransport;

const send = (method: string, ...args: (string | number)[]) => {
transport?.send(JSON.stringify({
id: createId(),
type: 6,
object: 'transport',
method: 'onEvent',
args: [method, ...args],
}));
};

return {
active: !!transport,
send,
};
};

export default useShell;
5 changes: 3 additions & 2 deletions src/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const Multiselect = require('./Multiselect');
const { default: MultiselectMenu } = require('./MultiselectMenu');
const { HorizontalNavBar, VerticalNavBar } = require('./NavBar');
const PaginationInput = require('./PaginationInput');
const { PlatformProvider, usePlatform } = require('./Platform');
const PlayIconCircleCentered = require('./PlayIconCircleCentered');
const Popup = require('./Popup');
const SearchBar = require('./SearchBar');
Expand Down Expand Up @@ -45,7 +46,6 @@ const useProfile = require('./useProfile');
const useStreamingServer = require('./useStreamingServer');
const useTorrent = require('./useTorrent');
const useTranslate = require('./useTranslate');
const platform = require('./platform');
const EventModal = require('./EventModal');

module.exports = {
Expand All @@ -68,6 +68,8 @@ module.exports = {
HorizontalNavBar,
VerticalNavBar,
PaginationInput,
PlatformProvider,
usePlatform,
PlayIconCircleCentered,
Popup,
SearchBar,
Expand Down Expand Up @@ -98,6 +100,5 @@ module.exports = {
useStreamingServer,
useTorrent,
useTranslate,
platform,
EventModal,
};
36 changes: 0 additions & 36 deletions src/common/platform.js

This file was deleted.

5 changes: 3 additions & 2 deletions src/routes/Addons/Addons.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { AddonDetailsModal, Button, Image, Multiselect, MainNavBars, TextInput, SearchBar, SharePrompt, ModalDialog, useBinaryState, withCoreSuspender } = require('stremio/common');
const { AddonDetailsModal, Button, Image, Multiselect, MainNavBars, TextInput, SearchBar, SharePrompt, ModalDialog, usePlatform, useBinaryState, withCoreSuspender } = require('stremio/common');
const Addon = require('./Addon');
const useInstalledAddons = require('./useInstalledAddons');
const useRemoteAddons = require('./useRemoteAddons');
Expand All @@ -15,6 +15,7 @@ const styles = require('./styles');

const Addons = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
const platform = usePlatform();
const installedAddons = useInstalledAddons(urlParams);
const remoteAddons = useRemoteAddons(urlParams);
const [addonDetailsTransportUrl, setAddonDetailsTransportUrl] = useAddonDetailsTransportUrl(urlParams, queryParams);
Expand Down Expand Up @@ -59,7 +60,7 @@ const Addons = ({ urlParams, queryParams }) => {
setAddonDetailsTransportUrl(event.dataset.addon.transportUrl);
}, [setAddonDetailsTransportUrl]);
const onAddonConfigure = React.useCallback((event) => {
window.open(event.dataset.addon.transportUrl.replace('manifest.json', 'configure'));
platform.openExternal(event.dataset.addon.transportUrl.replace('manifest.json', 'configure'));
}, []);
const closeAddonDetails = React.useCallback(() => {
setAddonDetailsTransportUrl(null);
Expand Down
5 changes: 3 additions & 2 deletions src/routes/Intro/PasswordResetModal/PasswordResetModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
const React = require('react');
const PropTypes = require('prop-types');
const { useRouteFocused } = require('stremio-router');
const { ModalDialog } = require('stremio/common');
const { ModalDialog, usePlatform } = require('stremio/common');
const CredentialsTextInput = require('../CredentialsTextInput');
const styles = require('./styles');

const PasswordResetModal = ({ email, onCloseRequest }) => {
const routeFocused = useRouteFocused();
const platform = usePlatform();
const [error, setError] = React.useState('');
const emailRef = React.useRef(null);
const goToPasswordReset = React.useCallback(() => {
emailRef.current.value.length > 0 && emailRef.current.validity.valid ?
window.open('https://www.strem.io/reset-password/' + emailRef.current.value, '_blank')
platform.openExternal('https://www.strem.io/reset-password/' + emailRef.current.value, '_blank')
:
setError('Invalid email');
}, []);
Expand Down
3 changes: 2 additions & 1 deletion src/routes/MetaDetails/StreamsList/Stream/Stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { t } = require('i18next');
const { Button, Image, useProfile, platform, useToast, Popup, useBinaryState } = require('stremio/common');
const { Button, Image, useProfile, usePlatform, useToast, Popup, useBinaryState } = require('stremio/common');
const { useServices } = require('stremio/services');
const { useRouteFocused } = require('stremio-router');
const StreamPlaceholder = require('./StreamPlaceholder');
Expand All @@ -14,6 +14,7 @@ const styles = require('./styles');
const Stream = ({ className, videoId, videoReleased, addonName, name, description, thumbnail, progress, deepLinks, ...props }) => {
const profile = useProfile();
const toast = useToast();
const platform = usePlatform();
const { core } = useServices();
const routeFocused = useRouteFocused();

Expand Down
5 changes: 3 additions & 2 deletions src/routes/Player/OptionsMenu/OptionsMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { useToast } = require('stremio/common');
const { usePlatform, useToast } = require('stremio/common');
const { useServices } = require('stremio/services');
const Option = require('./Option');
const styles = require('./styles');

const OptionsMenu = ({ className, stream, playbackDevices }) => {
const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
const toast = useToast();
const [streamingUrl, downloadUrl] = React.useMemo(() => {
return stream !== null ?
Expand Down Expand Up @@ -48,7 +49,7 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
}, [streamingUrl, downloadUrl]);
const onDownloadVideoButtonClick = React.useCallback(() => {
if (streamingUrl || downloadUrl) {
window.open(streamingUrl || downloadUrl);
platform.openExternal(streamingUrl || downloadUrl);
}
}, [streamingUrl, downloadUrl]);
const onExternalDeviceRequested = React.useCallback((deviceId) => {
Expand Down
Loading

0 comments on commit 024df8e

Please sign in to comment.