Feat: Discord RPC

This commit is contained in:
Youwes09
2026-03-29 15:38:39 -05:00
parent f6786def87
commit 0a11fe3982
12 changed files with 191 additions and 8 deletions
+19
View File
@@ -7,6 +7,7 @@
import { gql } from "./lib/client";
import { GET_DOWNLOAD_STATUS } from "./lib/queries";
import { store, addToast, setActiveDownloads, setSettingsOpen } from "./store/state.svelte";
import { initRpc, setIdle, clearReading, destroyRpc } from "./lib/discord";
import type { DownloadStatus, DownloadQueueItem } from "./lib/types";
import Layout from "./components/layout/Layout.svelte";
import Reader from "./components/reader/Reader.svelte";
@@ -263,6 +264,7 @@
cancelProbe = true;
unlistenResize();
unlistenScale();
destroyRpc();
if (store.settings.autoStartServer) invoke("kill_server").catch(() => {});
if (idleTimer) clearTimeout(idleTimer);
if (pollInterval) clearInterval(pollInterval);
@@ -277,6 +279,23 @@
return () => clearTimeout(timer);
});
$effect(() => {
if (store.settings.discordRpc) {
initRpc();
} else {
clearReading();
destroyRpc();
}
});
// When the reader closes, show idle presence.
$effect(() => {
if (!store.activeChapter) {
if (store.settings.discordRpc) setIdle();
}
});
function handleRetry() {
failed = false;
notConfigured = false;
+14
View File
@@ -5,6 +5,7 @@
import { FETCH_CHAPTER_PAGES, MARK_CHAPTER_READ, ENQUEUE_DOWNLOAD, ENQUEUE_CHAPTERS_DOWNLOAD } from "../../lib/queries";
import { store, closeReader, openReader, addHistory, updateSettings, checkAndMarkCompleted, setSettingsOpen } from "../../store/state.svelte";
import { matchesKeybind, toggleFullscreen, DEFAULT_KEYBINDS } from "../../lib/keybinds";
import { setReading } from "../../lib/discord";
import type { FitMode } from "../../store/state.svelte";
// ─── Constants ────────────────────────────────────────────────────────────────
@@ -190,6 +191,19 @@
: store.activeChapter
);
// ─── Discord RPC ──────────────────────────────────────────────────────────────
// displayChapter already handles both single/double (store.activeChapter) and
// longstrip auto-next (visibleChapterId) — so reacting to it here means RPC
// updates on every chapter transition regardless of reading mode.
$effect(() => {
const chapter = displayChapter;
const manga = store.activeManga;
if (store.settings.discordRpc && chapter && manga) {
setReading(manga, chapter);
}
});
const adjacent = $derived.by(() => {
const ref = displayChapter ?? store.activeChapter;
if (!ref || !store.activeChapterList.length) return { prev: null, next: null, remaining: [] };
+8 -1
View File
@@ -775,6 +775,13 @@
</div>
</div>
</div>
<div class="section">
<p class="section-title">Integrations</p>
<label class="toggle-row">
<div class="toggle-info"><span class="toggle-label">Discord Rich Presence</span><span class="toggle-desc">Show what you're reading in your Discord status</span></div>
<button role="switch" aria-checked={store.settings.discordRpc} aria-label="Discord Rich Presence" class="toggle" class:on={store.settings.discordRpc} onclick={() => updateSettings({ discordRpc: !store.settings.discordRpc })}><span class="toggle-thumb"></span></button>
</label>
</div>
</div>
@@ -1778,7 +1785,7 @@
<p class="section-title">Links</p>
<div class="about-block">
<a href="https://github.com/Youwes09/Moku" target="_blank" class="about-line" style="color:var(--accent-fg);text-decoration:none">GitHub </a>
<a href="https://discord.gg/cfncTbJ2" target="_blank" class="about-line" style="color:var(--accent-fg);text-decoration:none;margin-top:var(--sp-1)">Discord </a>
<a href="https://discord.gg/Jq3pwuNqPp" target="_blank" class="about-line" style="color:var(--accent-fg);text-decoration:none;margin-top:var(--sp-1)">Discord </a>
</div>
</div>
+81
View File
@@ -0,0 +1,81 @@
import { start, stop, setActivity, clearActivity } from "tauri-plugin-drpc";
import { Activity, Assets, Button, Timestamps } from "tauri-plugin-drpc/activity";
import type { Manga, Chapter } from "./types";
const APP_ID = "1487894643613106298";
const FALLBACK_IMAGE = "moku_logo";
function isPublicUrl(url: string | null | undefined): boolean {
return typeof url === "string" && url.startsWith("https://");
}
function resolveCoverImage(manga: Manga): string {
return isPublicUrl(manga.thumbnailUrl) ? manga.thumbnailUrl : FALLBACK_IMAGE;
}
function trunc(s: string, max = 128): string {
return s.length <= max ? s : `${s.slice(0, max - 1)}`;
}
function formatChapter(chapter: Chapter): string {
const n = chapter.chapterNumber;
return `Chapter ${Number.isInteger(n) ? n : n.toFixed(1)}`;
}
const BUTTONS = [
new Button("GitHub", "https://github.com/Youwes09/Moku"),
new Button("Discord", "https://discord.gg/Jq3pwuNqPp"),
];
export async function initRpc(): Promise<void> {
await start(APP_ID)
.then(() => console.log("[discord] RPC started"))
.catch((e) => console.error("[discord] initRpc failed:", e));
}
export async function setReading(manga: Manga, chapter: Chapter): Promise<void> {
const assets = new Assets()
.setLargeImage(resolveCoverImage(manga))
.setLargeText(trunc(manga.title))
.setSmallImage(FALLBACK_IMAGE)
.setSmallText("Moku");
const activity = new Activity()
.setDetails(trunc(manga.title))
.setState(`${formatChapter(chapter)} · Reading`)
.setAssets(assets)
.setTimestamps(new Timestamps(Date.now()));
activity.setButton(BUTTONS);
await setActivity(activity)
.then(() => console.log("[discord] reading →", manga.title, formatChapter(chapter)))
.catch((e) => console.error("[discord] setActivity failed:", e));
}
export async function setIdle(): Promise<void> {
const assets = new Assets()
.setLargeImage(FALLBACK_IMAGE)
.setLargeText("Moku");
const activity = new Activity()
.setDetails("Browsing")
.setAssets(assets)
.setTimestamps(new Timestamps(Date.now()));
activity.setButton(BUTTONS);
await setActivity(activity)
.then(() => console.log("[discord] idle"))
.catch((e) => console.error("[discord] setActivity failed (idle):", e));
}
export async function clearReading(): Promise<void> {
await clearActivity()
.then(() => console.log("[discord] activity cleared"))
.catch((e) => console.error("[discord] clearActivity failed:", e));
}
export async function destroyRpc(): Promise<void> {
await stop()
.then(() => console.log("[discord] RPC stopped"))
.catch((e) => console.error("[discord] destroyRpc failed:", e));
}
+2
View File
@@ -180,6 +180,7 @@ export interface Settings {
libraryCropCovers: boolean;
libraryPageSize: number;
showNsfw: boolean;
discordRpc: boolean;
chapterSortDir: ChapterSortDir;
chapterSortMode: ChapterSortMode;
chapterPageSize: number;
@@ -252,6 +253,7 @@ export const DEFAULT_SETTINGS: Settings = {
libraryCropCovers: true,
libraryPageSize: 48,
showNsfw: false,
discordRpc: false,
chapterSortDir: "desc",
chapterSortMode: "source",
chapterPageSize: 25,