mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Fix: Tauri-Plugin-HTTP for Windows Auth Support (Major WIP)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { onMount, untrack } from "svelte";
|
||||
import { Play, ArrowRight, ArrowLeft, BookOpen, Clock, Fire, TrendUp, CalendarBlank, CheckCircle, PushPin, X as XIcon, MagnifyingGlass, ListBullets } from "phosphor-svelte";
|
||||
import { gql, thumbUrl } from "../../lib/client";
|
||||
import { fetchAuthenticated } from "../../lib/auth";
|
||||
import { getBlobUrl } from "../../lib/imageCache";
|
||||
import Thumbnail from "../shared/Thumbnail.svelte";
|
||||
import { GET_LIBRARY, GET_CHAPTERS, GET_MANGA, GET_CATEGORIES } from "../../lib/queries";
|
||||
import { cache, CACHE_KEYS } from "../../lib/cache";
|
||||
@@ -121,22 +121,15 @@
|
||||
activeSlot?.kind === "continue" ? (activeSlot.entry?.thumbnailUrl ?? "") : ""
|
||||
);
|
||||
let heroThumb = $state("");
|
||||
const heroThumbCache = new Map<string, string>();
|
||||
$effect(() => {
|
||||
const path = heroThumbSrc;
|
||||
const mode = store.settings.serverAuthMode ?? "NONE";
|
||||
if (!path) { heroThumb = ""; return; }
|
||||
if (mode !== "BASIC_AUTH") { heroThumb = thumbUrl(path); return; }
|
||||
if (heroThumbCache.has(path)) { heroThumb = heroThumbCache.get(path)!; return; }
|
||||
heroThumb = "";
|
||||
fetchAuthenticated(thumbUrl(path), { method: "GET" })
|
||||
.then(r => r.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
heroThumbCache.set(path, url);
|
||||
heroThumb = url;
|
||||
})
|
||||
.catch(() => {});
|
||||
// Use tauri-plugin-http backed getBlobUrl which handles auth and bypasses CORS
|
||||
getBlobUrl(thumbUrl(path))
|
||||
.then(url => { heroThumb = url; })
|
||||
.catch(() => { heroThumb = ""; });
|
||||
});
|
||||
const heroTitle = $derived(activeSlot?.kind === "pinned" ? (activeSlot.manga?.title ?? "") : activeSlot?.kind === "continue" ? (activeSlot.entry?.mangaTitle ?? "") : "");
|
||||
const heroManga = $derived(activeSlot?.kind === "pinned" ? activeSlot.manga : activeSlot?.kind === "continue" ? libraryManga.find(m => m.id === activeSlot.entry?.mangaId) : null);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import type { LibrarySortMode, LibrarySortDir, LibraryStatusFilter, LibraryContentFilter } from "../../store/state.svelte";
|
||||
import type { Manga, Category, Chapter } from "../../lib/types";
|
||||
import ContextMenu, { type MenuEntry } from "../shared/ContextMenu.svelte";
|
||||
import Thumbnail from "../shared/Thumbnail.svelte";
|
||||
|
||||
const CARD_MIN_W = 130;
|
||||
const CARD_GAP = 16;
|
||||
@@ -920,7 +921,7 @@
|
||||
onpointerleave={onCardPointerLeave}
|
||||
>
|
||||
<div class="cover-wrap">
|
||||
<img src={thumbUrl(m.thumbnailUrl)} alt={m.title} class="cover" style="object-fit:{store.settings.libraryCropCovers ? 'cover' : 'contain'}" loading="lazy" decoding="async" draggable="false" />
|
||||
<Thumbnail src={m.thumbnailUrl} alt={m.title} class="cover" style="object-fit:{store.settings.libraryCropCovers ? 'cover' : 'contain'}" draggable="false" />
|
||||
{#if m.downloadCount}<span class="badge-dl">{m.downloadCount}</span>{/if}
|
||||
{#if m.unreadCount}<span class="badge-unread">{m.unreadCount}</span>{/if}
|
||||
{#if selectMode}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
CircleNotch, MagnifyingGlassMinus, MagnifyingGlassPlus,
|
||||
Bookmark, BookOpen, MonitorPlay, MapPin, Check,
|
||||
} from "phosphor-svelte";
|
||||
import { gql, thumbUrl } from "../../lib/client";
|
||||
import { fetchAuthenticated } from "../../lib/auth";
|
||||
import { gql, thumbUrl, plainThumbUrl } from "../../lib/client";
|
||||
import { getBlobUrl } from "../../lib/imageCache";
|
||||
import { store as appStore } from "../../store/state.svelte";
|
||||
import { FETCH_CHAPTER_PAGES, MARK_CHAPTER_READ, ENQUEUE_DOWNLOAD, ENQUEUE_CHAPTERS_DOWNLOAD } from "../../lib/queries";
|
||||
import { store, closeReader, openReader, addHistory, updateSettings, checkAndMarkCompleted, setSettingsOpen, addBookmark, removeBookmark, addMarker, removeMarker, updateMarker } from "../../store/state.svelte";
|
||||
@@ -43,16 +43,16 @@
|
||||
if (!inflight.has(chapterId)) {
|
||||
const p = gql<{ fetchChapterPages: { pages: string[] } }>(FETCH_CHAPTER_PAGES, { chapterId })
|
||||
.then(async d => {
|
||||
const rawUrls = d.fetchChapterPages.pages.map(thumbUrl);
|
||||
const mode = appStore.settings.serverAuthMode ?? "NONE";
|
||||
const urls = mode === "BASIC_AUTH"
|
||||
? await Promise.all(rawUrls.map(u =>
|
||||
fetchAuthenticated(u, { method: "GET" })
|
||||
.then(r => r.blob())
|
||||
.then(b => URL.createObjectURL(b))
|
||||
.catch(() => u)
|
||||
))
|
||||
: rawUrls;
|
||||
const rawUrls = d.fetchChapterPages.pages.map(p => plainThumbUrl(p));
|
||||
let urls: string[];
|
||||
if (mode === "BASIC_AUTH") {
|
||||
// Pre-fetch all pages via tauri-plugin-http (bypasses CORS + auth headers)
|
||||
// in parallel so they're cached and ready to display immediately
|
||||
urls = await Promise.all(rawUrls.map(u => getBlobUrl(u).catch(() => u)));
|
||||
} else {
|
||||
urls = rawUrls.map(u => thumbUrl(u));
|
||||
}
|
||||
pageCache.set(chapterId, urls);
|
||||
return urls;
|
||||
})
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from "svelte";
|
||||
import { thumbUrl } from "../../lib/client";
|
||||
import { fetchAuthenticated } from "../../lib/auth";
|
||||
import { thumbUrl, plainThumbUrl } from "../../lib/client";
|
||||
import { store } from "../../store/state.svelte";
|
||||
import { getBlobUrl } from "../../lib/imageCache";
|
||||
|
||||
let {
|
||||
src,
|
||||
@@ -22,40 +21,22 @@
|
||||
[key: string]: any;
|
||||
} = $props();
|
||||
|
||||
const blobCache = new Map<string, string>();
|
||||
const isAuth = $derived(store.settings.serverAuthMode === "BASIC_AUTH");
|
||||
|
||||
let resolved = $state("");
|
||||
let current = "";
|
||||
// Plain URL for non-auth users — fast, no overhead
|
||||
const plainResolved = $derived(src ? thumbUrl(src) : "");
|
||||
|
||||
// Blob URL for auth users — fetched with Authorization header
|
||||
let blobUrl = $state("");
|
||||
$effect(() => {
|
||||
const path = src;
|
||||
const mode = store.settings.serverAuthMode ?? "NONE";
|
||||
|
||||
if (path === current) return;
|
||||
current = path;
|
||||
|
||||
if (!path) { resolved = ""; return; }
|
||||
|
||||
if (mode !== "BASIC_AUTH") {
|
||||
resolved = thumbUrl(path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (blobCache.has(path)) {
|
||||
resolved = blobCache.get(path)!;
|
||||
return;
|
||||
}
|
||||
|
||||
resolved = "";
|
||||
fetchAuthenticated(thumbUrl(path), { method: "GET" })
|
||||
.then(r => r.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
blobCache.set(path, url);
|
||||
if (current === path) resolved = url;
|
||||
})
|
||||
.catch(() => {});
|
||||
if (!isAuth || !src) { blobUrl = ""; return; }
|
||||
const fullUrl = plainThumbUrl(src);
|
||||
getBlobUrl(fullUrl)
|
||||
.then(u => { blobUrl = u; })
|
||||
.catch(() => { blobUrl = ""; });
|
||||
});
|
||||
|
||||
const resolved = $derived(isAuth ? blobUrl || undefined : plainResolved || undefined);
|
||||
</script>
|
||||
|
||||
<img src={resolved} {alt} class={className} {loading} {decoding} {onerror} {...rest} />
|
||||
|
||||
+8
-16
@@ -10,25 +10,17 @@ function getServerUrl(): string {
|
||||
|
||||
function gqlUrl(): string { return `${getServerUrl()}/api/graphql`; }
|
||||
|
||||
export function thumbUrl(path: string): string {
|
||||
// Returns a clean absolute URL with no embedded credentials.
|
||||
export function plainThumbUrl(path: string): string {
|
||||
if (!path) return "";
|
||||
if (path.startsWith("http")) return path;
|
||||
return `${getServerUrl()}${path}`;
|
||||
}
|
||||
|
||||
const base = getServerUrl();
|
||||
const mode = store.settings.serverAuthMode;
|
||||
|
||||
if (mode === "BASIC_AUTH") {
|
||||
const user = store.settings.serverAuthUser?.trim() ?? "";
|
||||
const pass = store.settings.serverAuthPass?.trim() ?? "";
|
||||
if (user && pass) {
|
||||
const url = new URL(`${base}${path}`);
|
||||
url.username = user;
|
||||
url.password = pass;
|
||||
return url.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return `${base}${path}`;
|
||||
// Same as plainThumbUrl — credentials are never embedded in URLs.
|
||||
// Auth users load images via getBlobUrl (imageCache.ts) instead.
|
||||
export function thumbUrl(path: string): string {
|
||||
return plainThumbUrl(path);
|
||||
}
|
||||
|
||||
interface GQLResponse<T> {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
|
||||
import { store } from "../store/state.svelte";
|
||||
|
||||
const cache = new Map<string, string>();
|
||||
const inflight = new Map<string, Promise<string>>();
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
const mode = store.settings.serverAuthMode;
|
||||
if (mode === "BASIC_AUTH") {
|
||||
const user = store.settings.serverAuthUser?.trim() ?? "";
|
||||
const pass = store.settings.serverAuthPass?.trim() ?? "";
|
||||
if (user && pass) {
|
||||
return { Authorization: `Basic ${btoa(`${user}:${pass}`)}` };
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export async function getBlobUrl(url: string): Promise<string> {
|
||||
if (!url) return "";
|
||||
|
||||
const cached = cache.get(url);
|
||||
if (cached) return cached;
|
||||
|
||||
const existing = inflight.get(url);
|
||||
if (existing) return existing;
|
||||
|
||||
const promise = tauriFetch(url, {
|
||||
method: "GET",
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`${res.status}`);
|
||||
return res.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
cache.set(url, blobUrl);
|
||||
inflight.delete(url);
|
||||
return blobUrl;
|
||||
})
|
||||
.catch(err => {
|
||||
inflight.delete(url);
|
||||
throw err;
|
||||
});
|
||||
|
||||
inflight.set(url, promise);
|
||||
return promise;
|
||||
}
|
||||
Reference in New Issue
Block a user