mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
fix: make weel and touch passive
This commit is contained in:
+31
-17
@@ -1,4 +1,5 @@
|
|||||||
import { platformService } from '$lib/platform-service'
|
import { platformService } from '$lib/platform-service'
|
||||||
|
import { settingsState } from '$lib/state/settings.svelte'
|
||||||
import type { Manga } from '$lib/types/manga'
|
import type { Manga } from '$lib/types/manga'
|
||||||
import type { Chapter } from '$lib/types/chapter'
|
import type { Chapter } from '$lib/types/chapter'
|
||||||
|
|
||||||
@@ -9,11 +10,8 @@ const APP_BUTTONS = [
|
|||||||
|
|
||||||
const FALLBACK_IMAGE = 'moku_logo'
|
const FALLBACK_IMAGE = 'moku_logo'
|
||||||
|
|
||||||
let sessionStart: number | null = null
|
let sessionStart: number | null = null
|
||||||
|
let activeMangaId: number | null = null
|
||||||
function isPublicUrl(url: string | null | undefined): boolean {
|
|
||||||
return typeof url === 'string' && url.startsWith('https://')
|
|
||||||
}
|
|
||||||
|
|
||||||
function trunc(s: string, max = 128): string {
|
function trunc(s: string, max = 128): string {
|
||||||
return s.length <= max ? s : `${s.slice(0, max - 1)}…`
|
return s.length <= max ? s : `${s.slice(0, max - 1)}…`
|
||||||
@@ -24,6 +22,31 @@ function formatChapter(chapter: Chapter): string {
|
|||||||
return `Chapter ${Number.isInteger(n) ? n : n.toFixed(1)}`
|
return `Chapter ${Number.isInteger(n) ? n : n.toFixed(1)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suwayomi always returns the proxy path (/api/v1/manga/{id}/thumbnail), never the raw CDN URL.
|
||||||
|
// The proxy URL is only useful to Discord when the server is publicly reachable over HTTPS.
|
||||||
|
// For localhost setups cover art falls back to the app logo until Suwayomi exposes rawThumbnailUrl.
|
||||||
|
function resolveCoverUrl(manga: Manga): string {
|
||||||
|
const serverBase = (settingsState.settings.serverUrl ?? '').replace(/\/$/, '')
|
||||||
|
if (!serverBase.startsWith('https://')) return FALLBACK_IMAGE
|
||||||
|
const path = manga.thumbnailUrl?.startsWith('/') ? manga.thumbnailUrl : `/api/v1/manga/${manga.id}/thumbnail`
|
||||||
|
return `${serverBase}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPresence(manga: Manga, chapter: Chapter, coverUrl: string) {
|
||||||
|
return {
|
||||||
|
details: trunc(manga.title),
|
||||||
|
state: `${formatChapter(chapter)} · Reading`,
|
||||||
|
timestamps: { start: sessionStart ?? Date.now() },
|
||||||
|
assets: {
|
||||||
|
largeImage: coverUrl,
|
||||||
|
largeText: trunc(manga.title),
|
||||||
|
smallImage: FALLBACK_IMAGE,
|
||||||
|
smallText: 'Moku',
|
||||||
|
},
|
||||||
|
buttons: APP_BUTTONS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function initRpc(): Promise<void> {
|
export async function initRpc(): Promise<void> {
|
||||||
if (!platformService.isSupported('discord-rpc')) return
|
if (!platformService.isSupported('discord-rpc')) return
|
||||||
sessionStart = Date.now()
|
sessionStart = Date.now()
|
||||||
@@ -36,18 +59,9 @@ export async function destroyRpc(): Promise<void> {
|
|||||||
|
|
||||||
export async function setReading(manga: Manga, chapter: Chapter): Promise<void> {
|
export async function setReading(manga: Manga, chapter: Chapter): Promise<void> {
|
||||||
if (!platformService.isSupported('discord-rpc')) return
|
if (!platformService.isSupported('discord-rpc')) return
|
||||||
await platformService.setDiscordPresence({
|
|
||||||
details: trunc(manga.title),
|
activeMangaId = manga.id
|
||||||
state: `${formatChapter(chapter)} · Reading`,
|
await platformService.setDiscordPresence(buildPresence(manga, chapter, resolveCoverUrl(manga)))
|
||||||
timestamps: { start: sessionStart ?? Date.now() },
|
|
||||||
assets: {
|
|
||||||
largeImage: isPublicUrl(manga.thumbnailUrl) ? manga.thumbnailUrl : FALLBACK_IMAGE,
|
|
||||||
largeText: trunc(manga.title),
|
|
||||||
smallImage: FALLBACK_IMAGE,
|
|
||||||
smallText: 'Moku',
|
|
||||||
},
|
|
||||||
buttons: APP_BUTTONS,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setIdle(): Promise<void> {
|
export async function setIdle(): Promise<void> {
|
||||||
|
|||||||
+19
-16
@@ -158,30 +158,33 @@
|
|||||||
if (appState.status !== 'ready') return
|
if (appState.status !== 'ready') return
|
||||||
// capture phase so events from any component — including modals — reset the timer
|
// capture phase so events from any component — including modals — reset the timer
|
||||||
const onActivity = () => resetIdleTimer()
|
const onActivity = () => resetIdleTimer()
|
||||||
document.addEventListener('mousemove', onActivity, true)
|
document.addEventListener('mousemove', onActivity, true)
|
||||||
document.addEventListener('keydown', onActivity, true)
|
document.addEventListener('keydown', onActivity, true)
|
||||||
document.addEventListener('touchstart', onActivity, true)
|
document.addEventListener('touchstart', onActivity, true)
|
||||||
document.addEventListener('touchmove', onActivity, true) // sustained touch-scroll in reader
|
// passive:true tells the browser it can render scroll frames without waiting for the handler
|
||||||
document.addEventListener('wheel', onActivity, true) // mouse-wheel / trackpad scroll in reader
|
document.addEventListener('touchmove', onActivity, { capture: true, passive: true })
|
||||||
document.addEventListener('click', onActivity, true)
|
document.addEventListener('wheel', onActivity, { capture: true, passive: true })
|
||||||
|
document.addEventListener('click', onActivity, true)
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousemove', onActivity, true)
|
document.removeEventListener('mousemove', onActivity, true)
|
||||||
document.removeEventListener('keydown', onActivity, true)
|
document.removeEventListener('keydown', onActivity, true)
|
||||||
document.removeEventListener('touchstart', onActivity, true)
|
document.removeEventListener('touchstart', onActivity, true)
|
||||||
document.removeEventListener('touchmove', onActivity, true)
|
document.removeEventListener('touchmove', onActivity, { capture: true })
|
||||||
document.removeEventListener('wheel', onActivity, true)
|
document.removeEventListener('wheel', onActivity, { capture: true })
|
||||||
document.removeEventListener('click', onActivity, true)
|
document.removeEventListener('click', onActivity, true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function resetIdleTimer() {
|
function resetIdleTimer() {
|
||||||
if (idleTimer) clearTimeout(idleTimer)
|
if (idleTimer) { clearTimeout(idleTimer); idleTimer = null }
|
||||||
appState.idleSplash = false
|
if (appState.idleSplash) appState.idleSplash = false
|
||||||
// read the setting live so changes take effect without a restart
|
// read the setting live so changes take effect without a restart
|
||||||
const timeoutMs = (settingsState.settings.idleTimeoutMin ?? 5) * 60_000
|
// 0 means "Never" — skip the timer entirely
|
||||||
|
const mins = settingsState.settings.idleTimeoutMin ?? 5
|
||||||
|
if (mins === 0) return
|
||||||
idleTimer = setTimeout(() => {
|
idleTimer = setTimeout(() => {
|
||||||
if (appState.status === 'ready') appState.idleSplash = true
|
if (appState.status === 'ready') appState.idleSplash = true
|
||||||
}, timeoutMs)
|
}, mins * 60_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onIdleDismiss() { appState.idleSplash = false; resetIdleTimer() }
|
function onIdleDismiss() { appState.idleSplash = false; resetIdleTimer() }
|
||||||
|
|||||||
Reference in New Issue
Block a user