mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Fix: Emergency Push + Bookmark Feature (WIP)
This commit is contained in:
+11
-21
@@ -5,7 +5,7 @@ import type { Manga, Chapter } from "./types";
|
||||
const APP_ID = "1487894643613106298";
|
||||
const FALLBACK_IMAGE = "moku_logo";
|
||||
|
||||
let sessionStart: number | null = null; // ← captured once on init
|
||||
let sessionStart: number | null = null;
|
||||
|
||||
function isPublicUrl(url: string | null | undefined): boolean {
|
||||
return typeof url === "string" && url.startsWith("https://");
|
||||
@@ -34,10 +34,8 @@ const BUTTONS = [
|
||||
];
|
||||
|
||||
export async function initRpc(): Promise<void> {
|
||||
sessionStart = Date.now(); // ← set once here
|
||||
await start(APP_ID)
|
||||
.then(() => console.log("[discord] RPC started"))
|
||||
.catch((e) => console.error("[discord] initRpc failed:", e));
|
||||
sessionStart = Date.now();
|
||||
await start(APP_ID).catch(() => {});
|
||||
}
|
||||
|
||||
export async function setReading(manga: Manga, chapter: Chapter): Promise<void> {
|
||||
@@ -51,12 +49,10 @@ export async function setReading(manga: Manga, chapter: Chapter): Promise<void>
|
||||
.setDetails(trunc(manga.title))
|
||||
.setState(`${formatChapter(chapter)} · Reading`)
|
||||
.setAssets(assets)
|
||||
.setTimestamps(getTimestamps()); // ← reuses session start
|
||||
.setTimestamps(getTimestamps());
|
||||
activity.setButton(BUTTONS);
|
||||
|
||||
await setActivity(activity)
|
||||
.then(() => console.log("[discord] reading →", manga.title, formatChapter(chapter)))
|
||||
.catch((e) => console.error("[discord] setActivity failed:", e));
|
||||
await setActivity(activity).catch(() => {});
|
||||
}
|
||||
|
||||
export async function setIdle(): Promise<void> {
|
||||
@@ -67,23 +63,17 @@ export async function setIdle(): Promise<void> {
|
||||
const activity = new Activity()
|
||||
.setDetails("Browsing")
|
||||
.setAssets(assets)
|
||||
.setTimestamps(getTimestamps()); // ← reuses session start
|
||||
.setTimestamps(getTimestamps());
|
||||
activity.setButton(BUTTONS);
|
||||
|
||||
await setActivity(activity)
|
||||
.then(() => console.log("[discord] idle"))
|
||||
.catch((e) => console.error("[discord] setActivity failed (idle):", e));
|
||||
await setActivity(activity).catch(() => {});
|
||||
}
|
||||
|
||||
export async function clearReading(): Promise<void> {
|
||||
await clearActivity()
|
||||
.then(() => console.log("[discord] activity cleared"))
|
||||
.catch((e) => console.error("[discord] clearActivity failed:", e));
|
||||
await clearActivity().catch(() => {});
|
||||
}
|
||||
|
||||
export async function destroyRpc(): Promise<void> {
|
||||
sessionStart = null; // ← clean up on stop
|
||||
await stop()
|
||||
.then(() => console.log("[discord] RPC stopped"))
|
||||
.catch((e) => console.error("[discord] destroyRpc failed:", e));
|
||||
}
|
||||
sessionStart = null;
|
||||
await stop().catch(() => {});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface Keybinds {
|
||||
togglePageStyle: string;
|
||||
toggleFullscreen: string;
|
||||
openSettings: string;
|
||||
toggleBookmark: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_KEYBINDS: Keybinds = {
|
||||
@@ -26,6 +27,7 @@ export const DEFAULT_KEYBINDS: Keybinds = {
|
||||
togglePageStyle: "q",
|
||||
toggleFullscreen: "f",
|
||||
openSettings: "o",
|
||||
toggleBookmark: "m",
|
||||
};
|
||||
|
||||
export const KEYBIND_LABELS: Record<keyof Keybinds, string> = {
|
||||
@@ -40,6 +42,7 @@ export const KEYBIND_LABELS: Record<keyof Keybinds, string> = {
|
||||
togglePageStyle: "Toggle page style",
|
||||
toggleFullscreen: "Toggle fullscreen",
|
||||
openSettings: "Open settings",
|
||||
toggleBookmark: "Toggle bookmark",
|
||||
};
|
||||
|
||||
export function eventToKeybind(e: KeyboardEvent): string {
|
||||
|
||||
+59
-11
@@ -8,29 +8,77 @@ export function cn(...inputs: ClassValue[]) {
|
||||
// ── NSFW genre filtering ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Genre tags that indicate adult/mature content.
|
||||
* Checked case-insensitively against each manga's genre array.
|
||||
* Extend this set if additional tags need to be covered.
|
||||
* Default substrings used when no user-configured list is available.
|
||||
* The Settings > Content tab lets users add/remove entries from this list,
|
||||
* which is stored as settings.nsfwFilteredTags.
|
||||
*/
|
||||
const NSFW_GENRE_TAGS = new Set([
|
||||
export const DEFAULT_NSFW_TAGS = [
|
||||
"adult",
|
||||
"mature",
|
||||
"hentai",
|
||||
"ecchi",
|
||||
"erotica",
|
||||
"pornographic",
|
||||
"erotic", // catches "erotica", "erotic content", "erotic manga"
|
||||
"pornograph", // catches "pornographic", "pornography"
|
||||
"18+",
|
||||
"smut",
|
||||
"lemon",
|
||||
"explicit",
|
||||
]);
|
||||
"sexual violence",
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns true if the manga carries at least one genre tag that is considered
|
||||
* adult/mature. Used to enforce the `showNsfw` setting across all views.
|
||||
* Returns true if the manga carries at least one genre tag matching any of
|
||||
* the provided substrings (case-insensitive). Pass settings.nsfwFilteredTags
|
||||
* as the tag list; falls back to DEFAULT_NSFW_TAGS if omitted.
|
||||
*/
|
||||
export function isNsfwManga(manga: { genre?: string[] | null }): boolean {
|
||||
return (manga.genre ?? []).some((g) => NSFW_GENRE_TAGS.has(g.toLowerCase().trim()));
|
||||
export function isNsfwManga(
|
||||
manga: { genre?: string[] | null },
|
||||
tags: string[] = DEFAULT_NSFW_TAGS,
|
||||
): boolean {
|
||||
return (manga.genre ?? []).some((g) => {
|
||||
const normalized = g.toLowerCase().trim();
|
||||
return tags.some((sub) => normalized.includes(sub));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Single authoritative NSFW gate used by all views.
|
||||
*
|
||||
* Returns true when the manga should be HIDDEN. Checks in order:
|
||||
* 1. showNsfw disabled globally → skip everything, hide by source flag or genre match.
|
||||
* 2. Source is in blockedSourceIds → always hide regardless of showNsfw.
|
||||
* 3. Source is in allowedSourceIds → always show (bypasses isNsfw flag only, genre tags still apply).
|
||||
* 4. Source isNsfw flag → hide unless source is allowed.
|
||||
* 5. Genre tag match → hide.
|
||||
*
|
||||
* Usage: items.filter(m => !shouldHideNsfw(m, settings))
|
||||
*/
|
||||
export function shouldHideNsfw(
|
||||
manga: {
|
||||
genre?: string[] | null;
|
||||
source?: { id?: string; isNsfw?: boolean } | null;
|
||||
},
|
||||
settings: {
|
||||
showNsfw: boolean;
|
||||
nsfwFilteredTags: string[];
|
||||
nsfwAllowedSourceIds: string[];
|
||||
nsfwBlockedSourceIds: string[];
|
||||
},
|
||||
): boolean {
|
||||
const srcId = manga.source?.id;
|
||||
|
||||
// Explicit block always wins, even when showNsfw is on
|
||||
if (srcId && settings.nsfwBlockedSourceIds.includes(srcId)) return true;
|
||||
|
||||
// If NSFW is globally allowed, only explicit blocks apply
|
||||
if (settings.showNsfw) return false;
|
||||
|
||||
// Source is explicitly allowed — skip the isNsfw flag check, but still filter genres
|
||||
const sourceAllowed = !!(srcId && settings.nsfwAllowedSourceIds.includes(srcId));
|
||||
|
||||
if (!sourceAllowed && manga.source?.isNsfw) return true;
|
||||
|
||||
return isNsfwManga(manga, settings.nsfwFilteredTags);
|
||||
}
|
||||
|
||||
// ── Source deduplication ──────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user