mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-24 04:22:12 +00:00
Merge fe1f13010d into 392a496617
This commit is contained in:
commit
4afbc778b9
12 changed files with 294 additions and 20 deletions
|
|
@ -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, ServicesProvider, GamepadProvider } = require('stremio/services');
|
||||
const { Shell, Chromecast, Discord, ServicesProvider, GamepadProvider } = require('stremio/services');
|
||||
const { NotFound } = require('stremio/routes');
|
||||
const { FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useShell, useBinaryState, useProfile, withCoreSuspender, onFileDrop } = require('stremio/common');
|
||||
const { FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, DiscordProvider, CONSTANTS, useShell, useBinaryState, useProfile, withCoreSuspender, onFileDrop } = require('stremio/common');
|
||||
const ServicesToaster = require('./ServicesToaster');
|
||||
const DeepLinkHandler = require('./DeepLinkHandler');
|
||||
const SearchParamsHandler = require('./SearchParamsHandler');
|
||||
|
|
@ -32,6 +32,7 @@ const App = () => {
|
|||
const services = React.useMemo(() => {
|
||||
return {
|
||||
shell: new Shell(),
|
||||
discord: new Discord(),
|
||||
chromecast: new Chromecast(),
|
||||
};
|
||||
}, []);
|
||||
|
|
@ -101,6 +102,8 @@ const App = () => {
|
|||
services.chromecast.on('stateChanged', onChromecastStateChange);
|
||||
services.shell.start();
|
||||
services.chromecast.start();
|
||||
services.discord.init(services.shell);
|
||||
|
||||
window.services = services;
|
||||
return () => {
|
||||
services.shell.stop();
|
||||
|
|
@ -193,21 +196,23 @@ const App = () => {
|
|||
<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}
|
||||
/>
|
||||
<DiscordProvider>
|
||||
{
|
||||
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}
|
||||
/>
|
||||
</DiscordProvider>
|
||||
</FullscreenProvider>
|
||||
</ShortcutsProvider>
|
||||
</GamepadProvider>
|
||||
|
|
|
|||
129
src/common/Discord/Discord.tsx
Normal file
129
src/common/Discord/Discord.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useServices } from 'stremio/services';
|
||||
import useProfile from '../useProfile';
|
||||
|
||||
type Activity = {
|
||||
state: string,
|
||||
details?: string | null,
|
||||
image?: string | null,
|
||||
startTimestamp?: number | null,
|
||||
};
|
||||
|
||||
type DiscordContextValue = {
|
||||
available: boolean,
|
||||
connected: boolean,
|
||||
enabled: boolean,
|
||||
setActivity: (activity: Activity | null) => void,
|
||||
};
|
||||
|
||||
const DiscordContext = createContext<DiscordContextValue | null>(null);
|
||||
|
||||
const sameActivity = (first: Activity | null, second: Activity | null) => {
|
||||
return first?.state === second?.state &&
|
||||
first?.details === second?.details &&
|
||||
first?.image === second?.image &&
|
||||
first?.startTimestamp === second?.startTimestamp;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode,
|
||||
};
|
||||
|
||||
const DiscordProvider = ({ children }: Props) => {
|
||||
const { discord } = useServices();
|
||||
const profile = useProfile();
|
||||
const enabled = profile.settings?.discordRpcEnabled === true;
|
||||
const available = discord?.available === true;
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [activity, setActivityState] = useState<Activity | null>(null);
|
||||
const sentActivity = useRef<Activity | null>(null);
|
||||
const connectRequested = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!discord) return;
|
||||
|
||||
const onStatusChanged = (isConnected: boolean) => {
|
||||
connectRequested.current = false;
|
||||
setConnected(isConnected);
|
||||
};
|
||||
|
||||
discord.on('statusChanged', onStatusChanged);
|
||||
return () => {
|
||||
discord.off('statusChanged', onStatusChanged);
|
||||
};
|
||||
}, [discord]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!discord || !available) {
|
||||
connectRequested.current = false;
|
||||
setConnected(false);
|
||||
sentActivity.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
if (!connected && !connectRequested.current) {
|
||||
connectRequested.current = true;
|
||||
discord.connect();
|
||||
}
|
||||
} else {
|
||||
connectRequested.current = false;
|
||||
if (connected) {
|
||||
discord.disconnect();
|
||||
}
|
||||
sentActivity.current = null;
|
||||
}
|
||||
}, [available, connected, discord, enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!discord || !available || !enabled || !connected) return;
|
||||
|
||||
if (activity === null) {
|
||||
if (sentActivity.current !== null) {
|
||||
discord.clearActivity();
|
||||
sentActivity.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (sameActivity(sentActivity.current, activity)) return;
|
||||
|
||||
discord.setActivity(
|
||||
activity.state,
|
||||
activity.details || '',
|
||||
activity.image || null,
|
||||
activity.startTimestamp || null
|
||||
);
|
||||
sentActivity.current = activity;
|
||||
}, [activity, available, connected, discord, enabled]);
|
||||
|
||||
const setActivity = useCallback((nextActivity: Activity | null) => {
|
||||
setActivityState((currentActivity) => sameActivity(currentActivity, nextActivity) ? currentActivity : nextActivity);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
available,
|
||||
connected,
|
||||
enabled,
|
||||
setActivity,
|
||||
}), [available, connected, enabled, setActivity]);
|
||||
|
||||
return (
|
||||
<DiscordContext.Provider value={value}>
|
||||
{children}
|
||||
</DiscordContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useDiscord = () => {
|
||||
const value = useContext(DiscordContext);
|
||||
if (value === null) {
|
||||
throw new Error('useDiscord must be used inside DiscordProvider');
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export {
|
||||
DiscordProvider,
|
||||
useDiscord,
|
||||
};
|
||||
6
src/common/Discord/index.ts
Normal file
6
src/common/Discord/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { DiscordProvider, useDiscord } from './Discord';
|
||||
|
||||
export {
|
||||
DiscordProvider,
|
||||
useDiscord,
|
||||
};
|
||||
|
|
@ -6,6 +6,7 @@ const { PlatformProvider, usePlatform } = require('./Platform');
|
|||
const { ToastProvider, useToast } = require('./Toast');
|
||||
const { TooltipProvider, Tooltip } = require('./Tooltips');
|
||||
const { ShortcutsProvider, useShortcuts, onShortcut } = require('./Shortcuts');
|
||||
const { DiscordProvider, useDiscord } = require('./Discord');
|
||||
const CONSTANTS = require('./CONSTANTS');
|
||||
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
|
||||
const getVisibleChildrenRange = require('./getVisibleChildrenRange');
|
||||
|
|
@ -45,6 +46,8 @@ module.exports = {
|
|||
useToast,
|
||||
TooltipProvider,
|
||||
Tooltip,
|
||||
DiscordProvider,
|
||||
useDiscord,
|
||||
CONSTANTS,
|
||||
withCoreSuspender,
|
||||
useCoreSuspender,
|
||||
|
|
|
|||
|
|
@ -47,4 +47,3 @@ const Error = () => {
|
|||
};
|
||||
|
||||
export default Error;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const { useRouteFocused } = require('stremio-router');
|
|||
const { useCore } = require('stremio/core');
|
||||
const { useServices, useGamepad } = require('stremio/services');
|
||||
const { useContentGamepadNavigation } = require('stremio/services/GamepadNavigation');
|
||||
const { useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, useShell, usePlatform, onShortcut } = require('stremio/common');
|
||||
const { useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, useShell, usePlatform, onShortcut, useDiscord } = require('stremio/common');
|
||||
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
|
||||
const BufferingLoader = require('./BufferingLoader');
|
||||
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
|
||||
|
|
@ -56,6 +56,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
const routeFocused = useRouteFocused();
|
||||
const platform = usePlatform();
|
||||
const toast = useToast();
|
||||
const discord = useDiscord();
|
||||
|
||||
const [seeking, setSeeking] = React.useState(false);
|
||||
|
||||
|
|
@ -542,6 +543,28 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
}, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (video.state.stream === null || typeof player?.title !== 'string') {
|
||||
discord.setActivity(null);
|
||||
return;
|
||||
}
|
||||
|
||||
discord.setActivity({
|
||||
state: video.state.paused ? 'Paused' : 'Watching',
|
||||
details: player.title,
|
||||
image: player.metaItem?.poster || null,
|
||||
startTimestamp: !video.state.paused && typeof video.state.time === 'number' ?
|
||||
Math.floor((Date.now() / 1000) - video.state.time) :
|
||||
null,
|
||||
});
|
||||
}, [discord.setActivity, player?.title, player.metaItem?.poster, video.state.paused, video.state.stream]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
discord.setActivity(null);
|
||||
};
|
||||
}, [discord.setActivity]);
|
||||
|
||||
useMediaSession(video.state, player, fullscreen, onPlayRequested, onPauseRequested, onNextVideoRequested);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -9,3 +9,9 @@
|
|||
color: var(--color-trakt) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.discord-container {
|
||||
.option-icon {
|
||||
color: #5865f2 !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 're
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useCore } from 'stremio/core';
|
||||
import { Button } from 'stremio/components';
|
||||
import { usePlatform, useToast } from 'stremio/common';
|
||||
import { usePlatform, useToast, useDiscord } from 'stremio/common';
|
||||
import { Section, Option, Link } from '../components';
|
||||
import User from './User';
|
||||
import useDataExport from './useDataExport';
|
||||
|
|
@ -17,6 +17,7 @@ const General = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
|
|||
const core = useCore();
|
||||
const platform = usePlatform();
|
||||
const toast = useToast();
|
||||
const discord = useDiscord();
|
||||
const [dataExport, loadDataExport] = useDataExport();
|
||||
|
||||
const [traktAuthStarted, setTraktAuthStarted] = useState(false);
|
||||
|
|
@ -61,6 +62,19 @@ const General = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
|
|||
}
|
||||
}, [isTraktAuthenticated, profile.auth]);
|
||||
|
||||
const onToggleDiscord = useCallback(() => {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: {
|
||||
...profile.settings,
|
||||
discordRpcEnabled: !profile.settings.discordRpcEnabled
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [profile.settings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataExport.exportUrl) {
|
||||
platform.openExternal(dataExport.exportUrl);
|
||||
|
|
@ -134,6 +148,14 @@ const General = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
|
|||
{isTraktAuthenticated ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE')}
|
||||
</Button>
|
||||
</Option>
|
||||
{
|
||||
discord.available &&
|
||||
<Option className={styles['discord-container']} icon={'discord'} label={t('SETTINGS_DISCORD', { defaultValue: 'Discord Rich Presence' })}>
|
||||
<Button className={'button'} title={profile.settings.discordRpcEnabled ? t('MOBILE_DISCONNECT') : t('SETTINGS_DISCORD_CONNECT', { defaultValue: 'Connect' })} tabIndex={-1} onClick={onToggleDiscord}>
|
||||
{profile.settings.discordRpcEnabled ? t('MOBILE_DISCONNECT') : t('SETTINGS_DISCORD_CONNECT', { defaultValue: 'Connect' })}
|
||||
</Button>
|
||||
</Option>
|
||||
}
|
||||
</Section>
|
||||
</>;
|
||||
});
|
||||
|
|
|
|||
73
src/services/Discord/Discord.ts
Normal file
73
src/services/Discord/Discord.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (C) 2017-2025 Smart code 203358507
|
||||
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
type DiscordStatusData = {
|
||||
connected: boolean;
|
||||
};
|
||||
|
||||
class Discord {
|
||||
private events: EventEmitter;
|
||||
private connected: boolean;
|
||||
private shell: any;
|
||||
|
||||
constructor() {
|
||||
this.events = new EventEmitter();
|
||||
this.connected = false;
|
||||
this.shell = null;
|
||||
}
|
||||
|
||||
init(shellService: any): void {
|
||||
this.shell = shellService;
|
||||
|
||||
if (this.shell && this.shell.transport) {
|
||||
this.shell.transport.on('discord-status', (data: DiscordStatusData) => {
|
||||
this.connected = data.connected;
|
||||
this.events.emit('statusChanged', this.connected);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
if (this.shell && this.shell.active) {
|
||||
this.shell.transport.send('discord-connect', {});
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.shell && this.shell.active) {
|
||||
this.shell.transport.send('discord-disconnect', {});
|
||||
}
|
||||
}
|
||||
|
||||
setActivity(state: string, details: string, image?: string | null, startTimestamp?: number | null): void {
|
||||
if (this.shell && this.shell.active && this.connected) {
|
||||
this.shell.transport.send('discord-set-activity', {
|
||||
state,
|
||||
details,
|
||||
image: image || null,
|
||||
startTimestamp: startTimestamp || null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearActivity(): void {
|
||||
if (this.shell && this.shell.active && this.connected) {
|
||||
this.shell.transport.send('discord-clear-activity', {});
|
||||
}
|
||||
}
|
||||
|
||||
get available(): boolean {
|
||||
return this.shell && this.shell.active;
|
||||
}
|
||||
|
||||
on(name: string, listener: (data: any) => void): void {
|
||||
this.events.on(name, listener);
|
||||
}
|
||||
|
||||
off(name: string, listener: (data: any) => void): void {
|
||||
this.events.off(name, listener);
|
||||
}
|
||||
}
|
||||
|
||||
export default Discord;
|
||||
5
src/services/Discord/index.ts
Normal file
5
src/services/Discord/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2025 Smart code 203358507
|
||||
|
||||
import Discord from './Discord';
|
||||
|
||||
export default Discord;
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const Chromecast = require('./Chromecast');
|
||||
const Discord = require('./Discord');
|
||||
const { ServicesProvider, useServices } = require('./ServicesContext');
|
||||
const { GamepadProvider, useGamepad } = require('./GamepadContext');
|
||||
const Shell = require('./Shell');
|
||||
|
||||
module.exports = {
|
||||
Chromecast,
|
||||
Discord,
|
||||
ServicesProvider,
|
||||
useServices,
|
||||
Shell,
|
||||
|
|
|
|||
1
src/types/models/Ctx.d.ts
vendored
1
src/types/models/Ctx.d.ts
vendored
|
|
@ -18,6 +18,7 @@ type Settings = {
|
|||
audioPassthrough: boolean,
|
||||
autoFrameRateMatching: boolean,
|
||||
bingeWatching: boolean,
|
||||
discordRpcEnabled: boolean,
|
||||
hardwareDecoding: boolean,
|
||||
videoMode: string | null,
|
||||
escExitFullscreen: boolean,
|
||||
|
|
|
|||
Loading…
Reference in a new issue