Fix: Tauri-Plugin-HTTP for Windows Auth Support (Major WIP)

This commit is contained in:
Youwes09
2026-04-05 04:14:33 -05:00
parent 6446a19b2d
commit d989b2d67e
12 changed files with 321 additions and 75 deletions
+5 -12
View File
@@ -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);
+2 -1
View File
@@ -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}
+11 -11
View File
@@ -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;
})
+14 -33
View File
@@ -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
View File
@@ -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> {
+49
View File
@@ -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;
}