feat: allow loading mods without a Steam API key
Add fetchModFromUrl/fetchModFileFromUrl and accept a fileUrl query param on /items routes so the frontend can download saves directly from a search result's file_url, with no key required.
This commit is contained in:
+10
-4
@@ -18,11 +18,17 @@ export function searchWorkshop(q: string, page = 1): Promise<SearchResult> {
|
||||
}
|
||||
|
||||
/** Fetch a full parsed TTS save. */
|
||||
export function fetchMod(id: string): Promise<TTSMod> {
|
||||
return getJson<TTSMod>(`/items/${id}`);
|
||||
export function fetchMod(id: string, fileUrl?: string): Promise<TTSMod> {
|
||||
const params = new URLSearchParams();
|
||||
if (fileUrl) params.set('fileUrl', fileUrl);
|
||||
const qs = params.toString();
|
||||
return getJson<TTSMod>(`/items/${id}${qs ? `?${qs}` : ''}`);
|
||||
}
|
||||
|
||||
/** Build a URL for the raw save file download. */
|
||||
export function modFileUrl(id: string): string {
|
||||
return `${BASE}/items/${id}/file`;
|
||||
export function modFileUrl(id: string, fileUrl?: string): string {
|
||||
const params = new URLSearchParams();
|
||||
if (fileUrl) params.set('fileUrl', fileUrl);
|
||||
const qs = params.toString();
|
||||
return `/items/${id}/file${qs ? `?${qs}` : ''}`;
|
||||
}
|
||||
@@ -2,15 +2,17 @@ import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { collectRefs, flattenObjects } from '@tts/extract';
|
||||
import { useModStore } from '../stores/modStore';
|
||||
import { useSearchStore } from '../stores/searchStore';
|
||||
import { modFileUrl } from '../api';
|
||||
|
||||
export default function ModPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { mod, loading, error, load } = useModStore();
|
||||
const item = useSearchStore((s) => s.items.find((i) => i.id === id));
|
||||
|
||||
useEffect(() => {
|
||||
if (id) load(id);
|
||||
}, [id, load]);
|
||||
if (id) load(id, item?.fileUrl);
|
||||
}, [id, item?.fileUrl, load]);
|
||||
|
||||
if (loading) return <p className="text-sm text-zinc-400">Loading mod…</p>;
|
||||
if (error) return <p className="text-sm text-red-400">{error}</p>;
|
||||
@@ -28,7 +30,7 @@ export default function ModPage() {
|
||||
asset refs
|
||||
</p>
|
||||
<a
|
||||
href={modFileUrl(id!)}
|
||||
href={modFileUrl(id!, item?.fileUrl)}
|
||||
className="mt-3 inline-block rounded-lg bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-900 hover:bg-zinc-300"
|
||||
>
|
||||
Download save file
|
||||
|
||||
@@ -4,26 +4,28 @@ import { fetchMod } from '../api';
|
||||
|
||||
interface ModState {
|
||||
id: string | null;
|
||||
fileUrl?: string;
|
||||
mod: TTSMod | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
load: (id: string) => Promise<void>;
|
||||
load: (id: string, fileUrl?: string) => Promise<void>;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export const useModStore = create<ModState>((set) => ({
|
||||
id: null,
|
||||
fileUrl: undefined,
|
||||
mod: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
load: async (id) => {
|
||||
set({ loading: true, error: null, id });
|
||||
load: async (id, fileUrl) => {
|
||||
set({ loading: true, error: null, id, fileUrl });
|
||||
try {
|
||||
const mod = await fetchMod(id);
|
||||
const mod = await fetchMod(id, fileUrl);
|
||||
set({ mod, loading: false });
|
||||
} catch (err) {
|
||||
set({ loading: false, error: String(err) });
|
||||
}
|
||||
},
|
||||
clear: () => set({ id: null, mod: null, error: null, loading: false }),
|
||||
clear: () => set({ id: null, fileUrl: undefined, mod: null, error: null, loading: false }),
|
||||
}));
|
||||
Reference in New Issue
Block a user