This commit is contained in:
kveld9
2026-04-16 20:34:55 -03:00
parent b6812de26b
commit 28844e8456
12 changed files with 13111 additions and 740 deletions
+34 -15
View File
@@ -216,6 +216,19 @@ fn try_connect() -> Option<DiscordIpcClient> {
Some(client) Some(client)
} }
/// Apply a template string, replacing placeholders with actual values.
/// Supported placeholders: {title}, {artist}, {album}, {paused}
fn apply_template(template: &str, title: &str, artist: &str, album: Option<&str>, is_playing: bool) -> String {
let paused_text = if is_playing { "" } else { "Paused — " };
let album_text = album.unwrap_or("");
template
.replace("{title}", title)
.replace("{artist}", artist)
.replace("{album}", album_text)
.replace("{paused}", paused_text)
}
/// Update the Discord Rich Presence activity. /// Update the Discord Rich Presence activity.
/// ///
/// - `is_playing`: true = playing (timer shown), false = paused (no timer, state shows "Paused"). /// - `is_playing`: true = playing (timer shown), false = paused (no timer, state shows "Paused").
@@ -225,6 +238,12 @@ fn try_connect() -> Option<DiscordIpcClient> {
/// - `fetch_itunes_covers`: if true, fetch artwork from the iTunes Search API when no /// - `fetch_itunes_covers`: if true, fetch artwork from the iTunes Search API when no
/// `cover_art_url` is provided. If false (default), fall back to the Psysonic app icon /// `cover_art_url` is provided. If false (default), fall back to the Psysonic app icon
/// without making any external request — required for privacy opt-in. /// without making any external request — required for privacy opt-in.
/// - `details_template`: template string for the "details" field. Default: "{artist} - {title}".
/// Supported placeholders: {title}, {artist}, {album}, {paused}
/// - `state_template`: template string for the "state" field. Default: "{album}".
/// Supported placeholders: {title}, {artist}, {album}, {paused}
/// - `large_text_template`: template string for the large image tooltip. Default: "{album}".
/// Supported placeholders: {title}, {artist}, {album}, {paused}
#[tauri::command] #[tauri::command]
pub async fn discord_update_presence( pub async fn discord_update_presence(
state: tauri::State<'_, DiscordState>, state: tauri::State<'_, DiscordState>,
@@ -235,6 +254,9 @@ pub async fn discord_update_presence(
elapsed_secs: Option<f64>, elapsed_secs: Option<f64>,
cover_art_url: Option<String>, cover_art_url: Option<String>,
fetch_itunes_covers: bool, fetch_itunes_covers: bool,
details_template: Option<String>,
state_template: Option<String>,
large_text_template: Option<String>,
) -> Result<(), String> { ) -> Result<(), String> {
// Resolve artwork on a dedicated blocking thread — reqwest::blocking must not // Resolve artwork on a dedicated blocking thread — reqwest::blocking must not
// run on the Tokio async executor directly. // run on the Tokio async executor directly.
@@ -273,28 +295,25 @@ pub async fn discord_update_presence(
let client = guard.as_mut().unwrap(); let client = guard.as_mut().unwrap();
// Discord RPC only exposes two visible text rows (details + state). // Apply templates for the three configurable text fields.
// The application name "Psysonic" is shown automatically by Discord as the let details_str = details_template.as_deref().unwrap_or("{artist} - {title}");
// header line. Album goes into large_text — visible as a hover tooltip on let details_text = apply_template(details_str, &title, &artist, album.as_deref(), is_playing);
// the cover art icon.
let large_text = album.as_deref().unwrap_or("Psysonic"); let state_str = state_template.as_deref().unwrap_or("{album}");
let state_text = apply_template(state_str, &title, &artist, album.as_deref(), is_playing);
let large_text_str = large_text_template.as_deref().unwrap_or("{album}");
let large_text = apply_template(large_text_str, &title, &artist, album.as_deref(), is_playing);
let assets = if let Some(ref url) = artwork_url { let assets = if let Some(ref url) = artwork_url {
Assets::new() Assets::new()
.large_image(url.as_str()) .large_image(url.as_str())
.large_text(large_text) .large_text(&large_text)
} else { } else {
// Fallback to default Psysonic icon // Fallback to default Psysonic icon
Assets::new() Assets::new()
.large_image("psysonic") .large_image("psysonic")
.large_text(large_text) .large_text(&large_text)
};
// When paused, show "Paused" as the state text (replaces artist name).
let state_text: String = if is_playing {
artist.clone()
} else {
"Paused".to_string()
}; };
// ActivityType::Listening causes the Discord client to auto-start a running // ActivityType::Listening causes the Discord client to auto-start a running
@@ -308,7 +327,7 @@ pub async fn discord_update_presence(
let mut activity = Activity::new() let mut activity = Activity::new()
.activity_type(activity_type) .activity_type(activity_type)
.details(&title) .details(&details_text)
.state(&state_text) .state(&state_text)
.assets(assets); .assets(assets);
+1136 -36
View File
File diff suppressed because it is too large Load Diff
+1139 -36
View File
File diff suppressed because it is too large Load Diff
+1143 -36
View File
File diff suppressed because it is too large Load Diff
+1138 -36
View File
File diff suppressed because it is too large Load Diff
+1137 -36
View File
File diff suppressed because it is too large Load Diff
+1137 -36
View File
File diff suppressed because it is too large Load Diff
+1196 -36
View File
File diff suppressed because it is too large Load Diff
+1131 -36
View File
File diff suppressed because it is too large Load Diff
+3217 -372
View File
File diff suppressed because it is too large Load Diff
+680 -63
View File
File diff suppressed because it is too large Load Diff
+23 -2
View File
@@ -689,16 +689,28 @@ export function initAudioListeners(): () => void {
let discordPrevTrackId: string | null = null; let discordPrevTrackId: string | null = null;
let discordPrevIsPlaying: boolean | null = null; let discordPrevIsPlaying: boolean | null = null;
let discordPrevFetchCovers: boolean | null = null; let discordPrevFetchCovers: boolean | null = null;
let discordPrevTemplateDetails: string | null = null;
let discordPrevTemplateState: string | null = null;
let discordPrevTemplateLargeText: string | null = null;
function syncDiscord() { function syncDiscord() {
const { currentTrack, isPlaying, currentTime } = usePlayerStore.getState(); const { currentTrack, isPlaying, currentTime } = usePlayerStore.getState();
const { discordRichPresence, enableAppleMusicCoversDiscord } = useAuthStore.getState(); const {
discordRichPresence,
enableAppleMusicCoversDiscord,
discordTemplateDetails,
discordTemplateState,
discordTemplateLargeText,
} = useAuthStore.getState();
if (!discordRichPresence || !currentTrack) { if (!discordRichPresence || !currentTrack) {
if (discordPrevTrackId !== null) { if (discordPrevTrackId !== null) {
discordPrevTrackId = null; discordPrevTrackId = null;
discordPrevIsPlaying = null; discordPrevIsPlaying = null;
discordPrevFetchCovers = null; discordPrevFetchCovers = null;
discordPrevTemplateDetails = null;
discordPrevTemplateState = null;
discordPrevTemplateLargeText = null;
invoke('discord_clear_presence').catch(() => {}); invoke('discord_clear_presence').catch(() => {});
} }
return; return;
@@ -707,11 +719,17 @@ export function initAudioListeners(): () => void {
const trackChanged = currentTrack.id !== discordPrevTrackId; const trackChanged = currentTrack.id !== discordPrevTrackId;
const playingChanged = isPlaying !== discordPrevIsPlaying; const playingChanged = isPlaying !== discordPrevIsPlaying;
const coversSettingChanged = enableAppleMusicCoversDiscord !== discordPrevFetchCovers; const coversSettingChanged = enableAppleMusicCoversDiscord !== discordPrevFetchCovers;
if (!trackChanged && !playingChanged && !coversSettingChanged) return; const detailsTemplateChanged = discordTemplateDetails !== discordPrevTemplateDetails;
const stateTemplateChanged = discordTemplateState !== discordPrevTemplateState;
const largeTextTemplateChanged = discordTemplateLargeText !== discordPrevTemplateLargeText;
if (!trackChanged && !playingChanged && !coversSettingChanged && !detailsTemplateChanged && !stateTemplateChanged && !largeTextTemplateChanged) return;
discordPrevTrackId = currentTrack.id; discordPrevTrackId = currentTrack.id;
discordPrevIsPlaying = isPlaying; discordPrevIsPlaying = isPlaying;
discordPrevFetchCovers = enableAppleMusicCoversDiscord; discordPrevFetchCovers = enableAppleMusicCoversDiscord;
discordPrevTemplateDetails = discordTemplateDetails;
discordPrevTemplateState = discordTemplateState;
discordPrevTemplateLargeText = discordTemplateLargeText;
invoke('discord_update_presence', { invoke('discord_update_presence', {
title: currentTrack.title, title: currentTrack.title,
@@ -725,6 +743,9 @@ export function initAudioListeners(): () => void {
// iTunes cover fetching is only done when explicitly opted in. // iTunes cover fetching is only done when explicitly opted in.
coverArtUrl: null, coverArtUrl: null,
fetchItunesCovers: enableAppleMusicCoversDiscord, fetchItunesCovers: enableAppleMusicCoversDiscord,
detailsTemplate: discordTemplateDetails,
stateTemplate: discordTemplateState,
largeTextTemplate: discordTemplateLargeText,
}).catch(() => {}); }).catch(() => {});
} }