Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ require('spatial-navigation-polyfill');
const React = require('react');
const { useTranslation } = require('react-i18next');
const { Router } = require('stremio-router');
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { Core, Shell, Chromecast, Discord, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
Expand Down Expand Up @@ -33,6 +33,7 @@ const App = () => {
return {
core,
shell: new Shell(),
discord: new Discord(),
chromecast: new Chromecast(),
keyboardShortcuts: new KeyboardShortcuts(),
dragAndDrop: new DragAndDrop({ core })
Expand Down Expand Up @@ -95,6 +96,8 @@ const App = () => {
services.chromecast.start();
services.keyboardShortcuts.start();
services.dragAndDrop.start();
services.discord.init(services.shell);

window.services = services;
return () => {
services.core.stop();
Expand Down
2 changes: 2 additions & 0 deletions src/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const useAnimationFrame = require('./useAnimationFrame');
const useBinaryState = require('./useBinaryState');
const { default: useFullscreen } = require('./useFullscreen');
const { default: useInterval } = require('./useInterval');
const { default: useDiscord } = require('./useDiscord');
const useLiveRef = require('./useLiveRef');
const useModelState = require('./useModelState');
const useNotifications = require('./useNotifications');
Expand Down Expand Up @@ -56,6 +57,7 @@ module.exports = {
useBinaryState,
useFullscreen,
useInterval,
useDiscord,
useLiveRef,
useModelState,
useNotifications,
Expand Down
83 changes: 83 additions & 0 deletions src/common/useDiscord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (C) 2017-2025 Smart code 203358507

import { useCallback, useEffect, useState } from 'react';
import { useServices } from 'stremio/services';

type Service = {
available: boolean;
connected: boolean;
connect: () => void;
disconnect: () => void;
setActivity: (state: string, details: string, image?: string, startTimestamp?: number) => void;
clearActivity: () => void;
on: (name: string, listener: (data: unknown) => void) => void;
off: (name: string, listener: (data: unknown) => void) => void;
};

type Result = {
available: boolean;
connected: boolean;
connect: () => void;
disconnect: () => void;
setActivity: (state: string, details: string, image?: string, startTimestamp?: number) => void;
clearActivity: () => void;
};

const useDiscord = (): Result => {
const { discord } = useServices() as { discord?: Service };
const [connected, setConnected] = useState(discord?.connected ?? false);

useEffect(() => {
if (!discord) return;

const onStatusChanged = (isConnected: boolean) => {
setConnected(isConnected);
};

discord.on('statusChanged', onStatusChanged as (data: unknown) => void);

return () => {
discord.off('statusChanged', onStatusChanged as (data: unknown) => void);
};
}, [discord]);

const connect = useCallback(() => {
if (discord) {
discord.connect();
}
}, [discord]);

const disconnect = useCallback(() => {
if (discord) {
discord.disconnect();
}
}, [discord]);

const setActivity = useCallback((
state: string,
details: string,
image?: string,
startTimestamp?: number
) => {
if (discord) {
discord.setActivity(state, details, image, startTimestamp);
}
}, [discord]);

const clearActivity = useCallback(() => {
if (discord) {
discord.clearActivity();
}
}, [discord]);

return {
available: discord?.available ?? false,
connected,
connect,
disconnect,
setActivity,
clearActivity
};
};

export default useDiscord;
19 changes: 18 additions & 1 deletion src/routes/Player/Player.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const langs = require('langs');
const { useTranslation } = require('react-i18next');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { onFileDrop, useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform, onShortcut } = require('stremio/common');
const { onFileDrop, useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform, onShortcut, useDiscord } = require('stremio/common');
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
Expand Down Expand Up @@ -48,6 +48,7 @@ const Player = ({ urlParams, queryParams }) => {
const routeFocused = useRouteFocused();
const platform = usePlatform();
const toast = useToast();
const discord = useDiscord();

const [seeking, setSeeking] = React.useState(false);

Expand Down Expand Up @@ -572,6 +573,22 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]);

React.useEffect(() => {
if (!discord.connected || !discord.available) return;

const state = video.state.paused ? 'Paused' : 'Watching';

const startTimestamp = !video.state.paused && video.state.time !== null && video.state.duration !== null
? Math.floor((Date.now() / 1000) - video.state.time)
: null;

discord.setActivity(state, player?.title, player?.metaItem?.poster, startTimestamp);

return () => {
discord.clearActivity();
};
}, [discord.connected, discord.available, player?.title, player?.metaItem, video.state]);

// Media Session PlaybackState
React.useEffect(() => {
if (!navigator.mediaSession) return;
Expand Down
6 changes: 6 additions & 0 deletions src/routes/Settings/General/General.less
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@
color: var(--color-trakt) !important;
}
}

.discord-container {
.option-icon {
color: #5865F2 !important;
}
}
43 changes: 42 additions & 1 deletion src/routes/Settings/General/General.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 're
import { useTranslation } from 'react-i18next';
import { Button } from 'stremio/components';
import { useServices } from 'stremio/services';
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';
Expand All @@ -17,6 +17,7 @@ const General = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
const { core } = useServices();
const platform = usePlatform();
const toast = useToast();
const { available: discordAvailable, connected: isDiscordConnected, connect: connectDiscord, disconnect: disconnectDiscord } = useDiscord();
const [dataExport, loadDataExport] = useDataExport();

const [traktAuthStarted, setTraktAuthStarted] = useState(false);
Expand Down Expand Up @@ -61,12 +62,44 @@ const General = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
}
}, [isTraktAuthenticated, profile.auth]);

const onToggleDiscord = useCallback(() => {
if (isDiscordConnected) {
disconnectDiscord();
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
discordRpcEnabled: false
}
}
});
} else {
connectDiscord();
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
discordRpcEnabled: true
}
}
});
}
}, [isDiscordConnected, connectDiscord, disconnectDiscord]);

useEffect(() => {
if (dataExport.exportUrl) {
platform.openExternal(dataExport.exportUrl);
}
}, [dataExport.exportUrl]);

useEffect(() => {
if (discordAvailable && profile.settings.discordRpcEnabled && !isDiscordConnected) {
connectDiscord();
}
}, [discordAvailable, profile.settings.discordRpcEnabled]);

useEffect(() => {
if (isTraktAuthenticated && traktAuthStarted) {
core.transport.dispatch({
Expand Down Expand Up @@ -134,6 +167,14 @@ const General = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
{isTraktAuthenticated ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE')}
</Button>
</Option>
{
discordAvailable &&
<Option className={styles['discord-container']} icon={'discord'} label={t('SETTINGS_DISCORD')}>
<Button className={'button'} title={isDiscordConnected ? t('DISCONNECT') : t('SETTINGS_DISCORD_CONNECT')} tabIndex={-1} onClick={onToggleDiscord}>
{isDiscordConnected ? t('DISCONNECT') : t('SETTINGS_DISCORD_CONNECT')}
</Button>
</Option>
}
</Section>
</>;
});
Expand Down
73 changes: 73 additions & 0 deletions src/services/Discord/Discord.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions src/services/Discord/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (C) 2017-2025 Smart code 203358507

import Discord from './Discord';

export default Discord;
2 changes: 2 additions & 0 deletions src/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const Chromecast = require('./Chromecast');
const Core = require('./Core');
const Discord = require('./Discord');
const DragAndDrop = require('./DragAndDrop');
const KeyboardShortcuts = require('./KeyboardShortcuts');
const { ServicesProvider, useServices } = require('./ServicesContext');
Expand All @@ -10,6 +11,7 @@ const Shell = require('./Shell');
module.exports = {
Chromecast,
Core,
Discord,
DragAndDrop,
KeyboardShortcuts,
ServicesProvider,
Expand Down
1 change: 1 addition & 0 deletions src/types/models/Ctx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Settings = {
audioPassthrough: boolean,
autoFrameRateMatching: boolean,
bingeWatching: boolean,
discordRpcEnabled: boolean,
hardwareDecoding: boolean,
videoMode: string | null,
escExitFullscreen: boolean,
Expand Down