Add discord rich presence (#72)

This commit is contained in:
jeffvli 2023-10-23 06:58:39 -07:00
parent 2664a80851
commit 244c00c4c6
12 changed files with 391 additions and 7 deletions

View file

@ -0,0 +1,63 @@
import { Client, SetActivity } from '@xhayper/discord-rpc';
import { ipcMain } from 'electron';
const FEISHIN_DISCORD_APPLICATION_ID = '1165957668758900787';
let client: Client | null = null;
const createClient = (clientId?: string) => {
client = new Client({
clientId: clientId || FEISHIN_DISCORD_APPLICATION_ID,
});
client.login();
return client;
};
const setActivity = (activity: SetActivity) => {
if (client) {
client.user?.setActivity({
...activity,
});
}
};
const clearActivity = () => {
if (client) {
client.user?.clearActivity();
}
};
const quit = () => {
if (client) {
client?.destroy();
}
};
ipcMain.handle('discord-rpc-initialize', (_event, clientId?: string) => {
createClient(clientId);
});
ipcMain.handle('discord-rpc-set-activity', (_event, activity: SetActivity) => {
if (client) {
setActivity(activity);
}
});
ipcMain.handle('discord-rpc-clear-activity', () => {
if (client) {
clearActivity();
}
});
ipcMain.handle('discord-rpc-quit', () => {
quit();
});
export const discordRpc = {
clearActivity,
createClient,
quit,
setActivity,
};

View file

@ -2,3 +2,4 @@ import './lyrics';
import './player';
import './remote';
import './settings';
import './discord-rpc';

View file

@ -1,5 +1,6 @@
import { contextBridge } from 'electron';
import { browser } from './preload/browser';
import { discordRpc } from './preload/discord-rpc';
import { ipc } from './preload/ipc';
import { localSettings } from './preload/local-settings';
import { lyrics } from './preload/lyrics';
@ -10,6 +11,7 @@ import { utils } from './preload/utils';
contextBridge.exposeInMainWorld('electron', {
browser,
discordRpc,
ipc,
localSettings,
lyrics,

View file

@ -0,0 +1,28 @@
import { SetActivity } from '@xhayper/discord-rpc';
import { ipcRenderer } from 'electron';
const initialize = (clientId: string) => {
const client = ipcRenderer.invoke('discord-rpc-initialize', clientId);
return client;
};
const clearActivity = () => {
ipcRenderer.invoke('discord-rpc-clear-activity');
};
const setActivity = (activity: SetActivity) => {
ipcRenderer.invoke('discord-rpc-set-activity', activity);
};
const quit = () => {
ipcRenderer.invoke('discord-rpc-quit');
};
export const discordRpc = {
clearActivity,
initialize,
quit,
setActivity,
};
export type DiscordRpc = typeof discordRpc;

View file

@ -25,6 +25,7 @@ import { getMpvProperties } from '/@/renderer/features/settings/components/playb
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types';
import '@ag-grid-community/styles/ag-grid.css';
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
@ -37,7 +38,6 @@ const remote = isElectron() ? window.electron.remote : null;
export const App = () => {
const theme = useTheme();
const contentFont = useSettingsStore((state) => state.general.fontContent);
const accent = useSettingsStore((store) => store.general.accent);
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
const { type: playbackType } = usePlaybackSettings();
@ -46,6 +46,7 @@ export const App = () => {
const { clearQueue, restoreQueue } = useQueueControls();
const remoteSettings = useRemoteSettings();
const textStyleRef = useRef<HTMLStyleElement>();
useDiscordRpc();
useEffect(() => {
if (type === FontType.SYSTEM && system) {

View file

@ -0,0 +1,122 @@
/* eslint-disable consistent-return */
import isElectron from 'is-electron';
import { useCallback, useEffect, useRef } from 'react';
import {
useCurrentSong,
useCurrentStatus,
useDiscordSetttings,
usePlayerStore,
} from '/@/renderer/store';
import { SetActivity } from '@xhayper/discord-rpc';
import { PlayerStatus, ServerType } from '/@/renderer/types';
const discordRpc = isElectron() ? window.electron.discordRpc : null;
export const useDiscordRpc = () => {
const intervalRef = useRef(0);
const discordSettings = useDiscordSetttings();
const currentSong = useCurrentSong();
const currentStatus = useCurrentStatus();
const setActivity = useCallback(async () => {
if (!discordSettings.enableIdle && currentStatus === PlayerStatus.PAUSED) {
discordRpc?.clearActivity();
return;
}
const currentTime = usePlayerStore.getState().current.time;
const now = Date.now();
const start = currentTime ? Math.round(now - currentTime * 1000) : null;
const end =
currentSong?.duration && start ? Math.round(start + currentSong.duration) : null;
const artists = currentSong?.artists.map((artist) => artist.name).join(', ');
const activity: SetActivity = {
details: currentSong?.name.padEnd(2, ' ') || 'Idle',
instance: false,
largeImageKey: undefined,
largeImageText: currentSong?.album || 'Unknown album',
smallImageKey: undefined,
smallImageText: currentStatus,
state: artists && `By ${artists}`,
};
if (currentStatus === PlayerStatus.PLAYING) {
if (start && end) {
activity.startTimestamp = start;
activity.endTimestamp = end;
}
activity.smallImageKey = 'playing';
} else {
activity.smallImageKey = 'paused';
}
if (
currentSong?.serverType === ServerType.JELLYFIN &&
discordSettings.showServerImage &&
currentSong?.imageUrl
) {
activity.largeImageKey = currentSong?.imageUrl;
}
// Fall back to default icon if not set
if (!activity.largeImageKey) {
activity.largeImageKey = 'icon';
}
discordRpc?.setActivity(activity);
}, [currentSong, currentStatus, discordSettings.enableIdle, discordSettings.showServerImage]);
useEffect(() => {
const initializeDiscordRpc = async () => {
discordRpc?.initialize(discordSettings.clientId);
};
if (discordSettings.enabled) {
initializeDiscordRpc();
} else {
discordRpc?.quit();
}
return () => {
discordRpc?.quit();
};
}, [discordSettings.clientId, discordSettings.enabled]);
useEffect(() => {
if (discordSettings.enabled) {
let intervalSeconds = discordSettings.updateInterval;
if (intervalSeconds < 15) {
intervalSeconds = 15;
}
intervalRef.current = window.setInterval(setActivity, intervalSeconds * 1000);
return () => clearInterval(intervalRef.current);
}
return () => {};
}, [discordSettings.enabled, discordSettings.updateInterval, setActivity]);
// useEffect(() => {
// console.log(
// 'currentStatus, discordSettings.enableIdle',
// currentStatus,
// discordSettings.enableIdle,
// );
// if (discordSettings.enableIdle === false && currentStatus === PlayerStatus.PAUSED) {
// console.log('removing activity');
// clearActivity();
// clearInterval(intervalRef.current);
// }
// }, [
// clearActivity,
// currentStatus,
// discordSettings.enableIdle,
// discordSettings.enabled,
// setActivity,
// ]);
};

View file

@ -0,0 +1,95 @@
import isElectron from 'is-electron';
import { NumberInput, Switch, TextInput } from '/@/renderer/components';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { useDiscordSetttings, useSettingsStoreActions } from '/@/renderer/store';
export const DiscordSettings = () => {
const settings = useDiscordSetttings();
const { setSettings } = useSettingsStoreActions();
const discordOptions: SettingOption[] = [
{
control: (
<Switch
checked={settings.enabled}
onChange={(e) => {
setSettings({
discord: {
...settings,
enabled: e.currentTarget.checked,
},
});
}}
/>
),
description:
'Enable playback status in Discord rich presence. Image keys include: "icon", "playing", and "paused"',
isHidden: !isElectron(),
title: 'Discord rich presence',
},
{
control: (
<TextInput
defaultValue={settings.clientId}
onBlur={(e) => {
setSettings({
discord: {
...settings,
clientId: e.currentTarget.value,
},
});
}}
/>
),
description: 'The Discord application ID (defaults to 1165957668758900787)',
isHidden: !isElectron(),
title: 'Discord application ID',
},
{
control: (
<NumberInput
value={settings.updateInterval}
onChange={(e) => {
let value = e ? Number(e) : 0;
if (value < 15) {
value = 15;
}
setSettings({
discord: {
...settings,
updateInterval: value,
},
});
}}
/>
),
description: 'The time in seconds between each update (minimum 15 seconds)',
isHidden: !isElectron(),
title: 'Rich presence update interval (seconds)',
},
{
control: (
<Switch
checked={settings.enableIdle}
onChange={(e) => {
setSettings({
discord: {
...settings,
enableIdle: e.currentTarget.checked,
},
});
}}
/>
),
description: 'When enabled, the rich presence will update while player is idle',
isHidden: !isElectron(),
title: 'Show rich presence when idle',
},
];
return <SettingsSection options={discordOptions} />;
};

View file

@ -1,12 +1,15 @@
import { Divider, Stack } from '@mantine/core';
import { UpdateSettings } from '/@/renderer/features/settings/components/window/update-settings';
import { WindowSettings } from '/@/renderer/features/settings/components/window/window-settings';
import { DiscordSettings } from '/@/renderer/features/settings/components/window/discord-settings';
export const WindowTab = () => {
return (
<Stack spacing="md">
<WindowSettings />
<Divider />
<DiscordSettings />
<Divider />
<UpdateSettings />
</Stack>
);

View file

@ -8,11 +8,13 @@ import { Lyrics } from '/@/main/preload/lyrics';
import { Utils } from '/@/main/preload/utils';
import { LocalSettings } from '/@/main/preload/local-settings';
import { Ipc } from '/@/main/preload/ipc';
import { DiscordRpc } from '/@/main/preload/discord-rpc';
declare global {
interface Window {
electron: {
browser: any;
discordRpc: DiscordRpc;
ipc?: Ipc;
ipcRenderer: {
APP_RESTART(): void;

View file

@ -112,6 +112,13 @@ export enum BindingActions {
}
export interface SettingsState {
discord: {
clientId: string;
enableIdle: boolean;
enabled: boolean;
showServerImage: boolean;
updateInterval: number;
};
font: {
builtIn: string;
custom: string | null;
@ -216,6 +223,13 @@ const getPlatformDefaultWindowBarStyle = (): Platform => {
const platformDefaultWindowBarStyle: Platform = getPlatformDefaultWindowBarStyle();
const initialState: SettingsState = {
discord: {
clientId: '1165957668758900787',
enableIdle: false,
enabled: false,
showServerImage: false,
updateInterval: 15,
},
font: {
builtIn: 'Inter',
custom: null,
@ -558,3 +572,5 @@ export const useLyricsSettings = () => useSettingsStore((state) => state.lyrics,
export const useRemoteSettings = () => useSettingsStore((state) => state.remote, shallow);
export const useFontSettings = () => useSettingsStore((state) => state.font, shallow);
export const useDiscordSetttings = () => useSettingsStore((state) => state.discord, shallow);