418 lines
13 KiB
TypeScript
418 lines
13 KiB
TypeScript
import { config } from '$lib/config';
|
|
import { updateActivity } from '$lib/presence';
|
|
import { get, writable } from 'svelte/store';
|
|
|
|
export type DiscordPresenceButtonConfig = {
|
|
enabled: boolean;
|
|
label: string;
|
|
url: string;
|
|
};
|
|
|
|
export type DiscordPresenceConfig = {
|
|
// Idle (Launcher) Configuration
|
|
idleDetails: string;
|
|
idleState: string;
|
|
idleLargeImageKey: string;
|
|
idleLargeImageText: string;
|
|
idleShowTime: boolean;
|
|
idleShowUserAvatar: boolean;
|
|
|
|
// Playing (osu!) Configuration
|
|
playingDetails: string;
|
|
playingState: string;
|
|
playingShowBeatmapCover: boolean;
|
|
playingLargeImageKey: string;
|
|
playingLargeImageText: string;
|
|
playingShowUserAvatar: boolean;
|
|
playingSmallImageText: string;
|
|
playingShowTime: boolean;
|
|
|
|
// Contextual Buttons
|
|
idleButton1: DiscordPresenceButtonConfig;
|
|
playingButton1: DiscordPresenceButtonConfig;
|
|
|
|
// Secondary Button
|
|
button2: DiscordPresenceButtonConfig;
|
|
};
|
|
|
|
export const DEFAULT_DISCORD_PRESENCE_CONFIG: DiscordPresenceConfig = {
|
|
idleDetails: 'In Launcher',
|
|
idleState: 'Chilling...',
|
|
idleLargeImageKey: 'ezppfarm',
|
|
idleLargeImageText: 'EZPPFarm Launcher',
|
|
idleShowTime: true,
|
|
idleShowUserAvatar: true,
|
|
|
|
playingDetails: '[{mode}] {action}',
|
|
playingState: '{beatmap}',
|
|
playingShowBeatmapCover: true,
|
|
playingLargeImageKey: 'ezppfarm',
|
|
playingLargeImageText: '{beatmap}',
|
|
playingShowUserAvatar: true,
|
|
playingSmallImageText: '{username} (#{rank})',
|
|
playingShowTime: true,
|
|
|
|
idleButton1: {
|
|
enabled: true,
|
|
label: 'Download Launcher',
|
|
url: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest',
|
|
},
|
|
playingButton1: {
|
|
enabled: true,
|
|
label: 'View Beatmap',
|
|
url: '{beatmapUrl}',
|
|
},
|
|
button2: {
|
|
enabled: true,
|
|
label: 'Join EZPPFarm',
|
|
url: 'https://ez-pp.farm/discord',
|
|
},
|
|
};
|
|
|
|
export type DiscordPresencePreset = {
|
|
name: string;
|
|
description: string;
|
|
config: Partial<DiscordPresenceConfig>;
|
|
};
|
|
|
|
export const DISCORD_PRESENCE_PRESETS: DiscordPresencePreset[] = [
|
|
{
|
|
name: 'EZPPFarm Default',
|
|
description: 'Standard presence with beatmap covers, actions, and official links.',
|
|
config: DEFAULT_DISCORD_PRESENCE_CONFIG,
|
|
},
|
|
{
|
|
name: 'Clean & Minimal',
|
|
description: 'Clean look without buttons or cluttered stats.',
|
|
config: {
|
|
idleDetails: 'In Launcher',
|
|
idleState: 'Chilling',
|
|
idleShowTime: true,
|
|
idleShowUserAvatar: false,
|
|
playingDetails: '{action}',
|
|
playingState: '{beatmap}',
|
|
playingShowBeatmapCover: true,
|
|
playingShowUserAvatar: false,
|
|
playingShowTime: true,
|
|
idleButton1: { enabled: false, label: '', url: '' },
|
|
playingButton1: { enabled: false, label: '', url: '' },
|
|
button2: { enabled: false, label: '', url: '' },
|
|
},
|
|
},
|
|
{
|
|
name: 'Competitive Stats',
|
|
description: 'Highlights your rank, performance points, and live accuracy.',
|
|
config: {
|
|
idleDetails: '{username} · #{rank} ({pp}pp)',
|
|
idleState: 'Preparing to farm...',
|
|
idleShowTime: true,
|
|
idleShowUserAvatar: true,
|
|
playingDetails: '[{mode}] {action} · {accuracy}',
|
|
playingState: '{beatmap}',
|
|
playingShowBeatmapCover: true,
|
|
playingShowUserAvatar: true,
|
|
playingSmallImageText: '{username} (#{rank} · {pp}pp)',
|
|
playingShowTime: true,
|
|
idleButton1: {
|
|
enabled: true,
|
|
label: 'View Profile',
|
|
url: '{profileUrl}',
|
|
},
|
|
playingButton1: {
|
|
enabled: true,
|
|
label: 'View Beatmap',
|
|
url: '{beatmapUrl}',
|
|
},
|
|
button2: {
|
|
enabled: true,
|
|
label: 'View Profile',
|
|
url: '{profileUrl}',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'Streamer / Privacy',
|
|
description: 'Hides exact map titles, player rank, and avatar for privacy.',
|
|
config: {
|
|
idleDetails: 'osu!',
|
|
idleState: 'In Launcher',
|
|
idleShowTime: false,
|
|
idleShowUserAvatar: false,
|
|
playingDetails: 'Playing osu!',
|
|
playingState: 'Offline practice',
|
|
playingShowBeatmapCover: false,
|
|
playingShowUserAvatar: false,
|
|
playingLargeImageKey: 'ezppfarm',
|
|
playingLargeImageText: 'osu!',
|
|
playingSmallImageText: '',
|
|
playingShowTime: false,
|
|
idleButton1: { enabled: false, label: '', url: '' },
|
|
playingButton1: { enabled: false, label: '', url: '' },
|
|
button2: { enabled: false, label: '', url: '' },
|
|
},
|
|
},
|
|
];
|
|
|
|
export type PresenceVariableContext = {
|
|
username?: string;
|
|
id?: string;
|
|
rank?: string | number;
|
|
pp?: string | number;
|
|
accuracy?: string | number;
|
|
mode?: string;
|
|
action?: string;
|
|
beatmap?: string;
|
|
artist?: string;
|
|
title?: string;
|
|
diff?: string;
|
|
version?: string;
|
|
beatmapUrl?: string;
|
|
profileUrl?: string;
|
|
};
|
|
|
|
export const interpolatePresenceString = (
|
|
template: string,
|
|
context: PresenceVariableContext
|
|
): string => {
|
|
if (!template) return '';
|
|
return template
|
|
.replaceAll('{username}', context.username ?? 'Player')
|
|
.replaceAll(
|
|
'{rank}',
|
|
context.rank !== undefined && context.rank !== '' ? String(context.rank) : '-'
|
|
)
|
|
.replaceAll('{pp}', context.pp !== undefined && context.pp !== '' ? String(context.pp) : '0')
|
|
.replaceAll(
|
|
'{accuracy}',
|
|
context.accuracy !== undefined && context.accuracy !== '' ? String(context.accuracy) : '100%'
|
|
)
|
|
.replaceAll('{mode}', context.mode ?? 'osu!')
|
|
.replaceAll('{action}', context.action ?? 'Playing...')
|
|
.replaceAll('{beatmap}', context.beatmap ?? 'Idle...')
|
|
.replaceAll('{artist}', context.artist ?? '')
|
|
.replaceAll('{title}', context.title ?? '')
|
|
.replaceAll('{diff}', context.diff ?? '')
|
|
.replaceAll(
|
|
'{version}',
|
|
context.version !== undefined && context.version !== '' ? String(context.version) : ''
|
|
)
|
|
.replaceAll('{beatmapUrl}', context.beatmapUrl ?? 'https://ez-pp.farm')
|
|
.replaceAll(
|
|
'{profileUrl}',
|
|
context.profileUrl ??
|
|
(context.id ? `https://ez-pp.farm/u/${context.id}` : 'https://ez-pp.farm')
|
|
);
|
|
};
|
|
|
|
export const discordPresenceCustomConfig = writable<DiscordPresenceConfig>(
|
|
DEFAULT_DISCORD_PRESENCE_CONFIG
|
|
);
|
|
|
|
export const loadDiscordPresenceConfig = async (): Promise<DiscordPresenceConfig> => {
|
|
try {
|
|
const raw = await config
|
|
.value<Partial<DiscordPresenceConfig>>('discord_presence_config')
|
|
.get({});
|
|
const merged: DiscordPresenceConfig = {
|
|
...DEFAULT_DISCORD_PRESENCE_CONFIG,
|
|
...raw,
|
|
idleButton1: { ...DEFAULT_DISCORD_PRESENCE_CONFIG.idleButton1, ...(raw?.idleButton1 ?? {}) },
|
|
playingButton1: {
|
|
...DEFAULT_DISCORD_PRESENCE_CONFIG.playingButton1,
|
|
...(raw?.playingButton1 ?? {}),
|
|
},
|
|
button2: { ...DEFAULT_DISCORD_PRESENCE_CONFIG.button2, ...(raw?.button2 ?? {}) },
|
|
};
|
|
discordPresenceCustomConfig.set(merged);
|
|
return merged;
|
|
} catch {
|
|
return DEFAULT_DISCORD_PRESENCE_CONFIG;
|
|
}
|
|
};
|
|
|
|
export const saveDiscordPresenceConfig = async (
|
|
newConfig: DiscordPresenceConfig
|
|
): Promise<void> => {
|
|
discordPresenceCustomConfig.set(newConfig);
|
|
try {
|
|
await config.value<DiscordPresenceConfig>('discord_presence_config').set(newConfig);
|
|
} catch (err) {
|
|
console.error('Failed to save discord presence config:', err);
|
|
}
|
|
};
|
|
|
|
export type IdlePresenceParams = {
|
|
user?: { id?: number | string; name?: string } | null;
|
|
version?: string;
|
|
rank?: number | string;
|
|
pp?: number | string;
|
|
accuracy?: number | string;
|
|
};
|
|
|
|
export type PlayingPresenceParams = {
|
|
user?: { id?: number | string; name?: string } | null;
|
|
mode?: string;
|
|
action?: string;
|
|
beatmap?: string;
|
|
artist?: string;
|
|
title?: string;
|
|
diff?: string;
|
|
version?: string;
|
|
beatmapCoverUrl?: string | null;
|
|
beatmapUrl?: string | null;
|
|
profileUrl?: string | null;
|
|
rank?: number | string;
|
|
pp?: number | string;
|
|
accuracy?: number | string;
|
|
};
|
|
|
|
const isValidHttpUrl = (url: string | undefined): boolean => {
|
|
if (!url) return false;
|
|
return url.startsWith('http://') || url.startsWith('https://');
|
|
};
|
|
|
|
export const pushIdlePresenceFromConfig = async (
|
|
params?: IdlePresenceParams,
|
|
cfg?: DiscordPresenceConfig
|
|
): Promise<void> => {
|
|
const currentCfg = cfg ?? get(discordPresenceCustomConfig);
|
|
const context: PresenceVariableContext = {
|
|
username: params?.user?.name,
|
|
id: params?.user?.id ? String(params.user.id) : undefined,
|
|
rank: params?.rank,
|
|
pp: params?.pp,
|
|
accuracy: params?.accuracy,
|
|
version: params?.version,
|
|
profileUrl: params?.user?.id ? `https://ez-pp.farm/u/${params.user.id}` : 'https://ez-pp.farm',
|
|
};
|
|
|
|
const details = interpolatePresenceString(currentCfg.idleDetails, context);
|
|
const state = interpolatePresenceString(currentCfg.idleState, context);
|
|
const largeImageKey = currentCfg.idleLargeImageKey || 'ezppfarm';
|
|
const largeImageText = interpolatePresenceString(currentCfg.idleLargeImageText, context);
|
|
|
|
let smallImageKey: string | null = null;
|
|
let smallImageText: string | null = null;
|
|
if (currentCfg.idleShowUserAvatar && params?.user?.id) {
|
|
smallImageKey = `https://a.ez-pp.farm/${params.user.id}`;
|
|
smallImageText = params.user.name ?? 'Player';
|
|
}
|
|
|
|
let button1: { label: string; url: string } | null = null;
|
|
if (currentCfg.idleButton1.enabled && currentCfg.idleButton1.label.trim()) {
|
|
const rawUrl = interpolatePresenceString(currentCfg.idleButton1.url, context);
|
|
if (isValidHttpUrl(rawUrl)) {
|
|
button1 = {
|
|
label: interpolatePresenceString(currentCfg.idleButton1.label, context),
|
|
url: rawUrl,
|
|
};
|
|
}
|
|
}
|
|
|
|
let button2: { label: string; url: string } | null = null;
|
|
if (currentCfg.button2.enabled && currentCfg.button2.label.trim()) {
|
|
const rawUrl = interpolatePresenceString(currentCfg.button2.url, context);
|
|
if (isValidHttpUrl(rawUrl)) {
|
|
button2 = {
|
|
label: interpolatePresenceString(currentCfg.button2.label, context),
|
|
url: rawUrl,
|
|
};
|
|
}
|
|
}
|
|
|
|
try {
|
|
await updateActivity({
|
|
state,
|
|
details,
|
|
largeImageKey,
|
|
largeImageText,
|
|
smallImageKey,
|
|
smallImageText,
|
|
button1,
|
|
button2,
|
|
showTimestamp: currentCfg.idleShowTime,
|
|
});
|
|
} catch (err) {
|
|
console.warn('Failed to push idle Discord presence:', err);
|
|
}
|
|
};
|
|
|
|
export const pushPlayingPresenceFromConfig = async (
|
|
params: PlayingPresenceParams,
|
|
cfg?: DiscordPresenceConfig
|
|
): Promise<void> => {
|
|
const currentCfg = cfg ?? get(discordPresenceCustomConfig);
|
|
const context: PresenceVariableContext = {
|
|
username: params.user?.name,
|
|
id: params.user?.id ? String(params.user.id) : undefined,
|
|
rank: params.rank,
|
|
pp: params.pp,
|
|
accuracy: params.accuracy,
|
|
mode: params.mode,
|
|
action: params.action,
|
|
beatmap: params.beatmap,
|
|
artist: params.artist,
|
|
title: params.title,
|
|
diff: params.diff,
|
|
version: params.version,
|
|
beatmapUrl: params.beatmapUrl ?? 'https://ez-pp.farm',
|
|
profileUrl:
|
|
params.profileUrl ??
|
|
(params.user?.id ? `https://ez-pp.farm/u/${params.user.id}` : 'https://ez-pp.farm'),
|
|
};
|
|
|
|
const details = interpolatePresenceString(currentCfg.playingDetails, context);
|
|
const state = interpolatePresenceString(currentCfg.playingState, context);
|
|
|
|
let largeImageKey = currentCfg.playingLargeImageKey || 'ezppfarm';
|
|
if (currentCfg.playingShowBeatmapCover && params.beatmapCoverUrl) {
|
|
largeImageKey = params.beatmapCoverUrl;
|
|
}
|
|
const largeImageText = interpolatePresenceString(currentCfg.playingLargeImageText, context);
|
|
|
|
let smallImageKey: string | null = null;
|
|
let smallImageText: string | null = null;
|
|
if (currentCfg.playingShowUserAvatar && params.user?.id) {
|
|
smallImageKey = `https://a.ez-pp.farm/${params.user.id}`;
|
|
smallImageText = interpolatePresenceString(currentCfg.playingSmallImageText, context);
|
|
}
|
|
|
|
let button1: { label: string; url: string } | null = null;
|
|
if (currentCfg.playingButton1.enabled && currentCfg.playingButton1.label.trim()) {
|
|
const rawUrl = interpolatePresenceString(currentCfg.playingButton1.url, context);
|
|
if (isValidHttpUrl(rawUrl)) {
|
|
button1 = {
|
|
label: interpolatePresenceString(currentCfg.playingButton1.label, context),
|
|
url: rawUrl,
|
|
};
|
|
}
|
|
}
|
|
|
|
let button2: { label: string; url: string } | null = null;
|
|
if (currentCfg.button2.enabled && currentCfg.button2.label.trim()) {
|
|
const rawUrl = interpolatePresenceString(currentCfg.button2.url, context);
|
|
if (isValidHttpUrl(rawUrl)) {
|
|
button2 = {
|
|
label: interpolatePresenceString(currentCfg.button2.label, context),
|
|
url: rawUrl,
|
|
};
|
|
}
|
|
}
|
|
|
|
try {
|
|
await updateActivity({
|
|
state,
|
|
details,
|
|
largeImageKey,
|
|
largeImageText,
|
|
smallImageKey,
|
|
smallImageText,
|
|
button1,
|
|
button2,
|
|
showTimestamp: currentCfg.playingShowTime,
|
|
});
|
|
} catch (err) {
|
|
console.warn('Failed to push playing Discord presence:', err);
|
|
}
|
|
};
|