fix all linting issues and upgrade the rest of devDependencies

now i should probably test the whole app waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaauuuuuuuuuugugghghghghghghghghg
This commit is contained in:
IRHM
2026-07-29 07:31:53 +01:00
committed by momi
parent d32a1ecb39
commit 2202b11e39
81 changed files with 3317 additions and 2977 deletions
+14
View File
@@ -10,9 +10,12 @@ These changes are awaiting release:
- Login: Display error when request to get available auth providers fails.
- Removed some legacy code that still thought there was a `store.watchedList`.
- DropDown: Fix whole page scrolling when pressing letter to scroll to first relevant dropdown item (only the dropdown list should scroll now, which is less jarring).
## Maintenance
I have been slacking in this department, so there has been a lot of house cleaning.
- Package.json: Use exact version for all packages.
- .npmrc: Enable `ignore-scripts` (still on npm 11) and set `min-release-age=14`.
- Workflows: Upgrade action versions & set Node version to `24`.
@@ -26,8 +29,19 @@ These changes are awaiting release:
- prettier-plugin-svelte: 3.4.0 -> 4.1.1
- svelte-eslint-parser: 0.42.0 -> 1.8.0
- typescript-eslint: 8.32.1 -> 8.63.0
- @sveltejs/adapter-node: 5.2.12 -> 5.5.7
- @sveltejs/kit: 2.21.0 -> 2.69.3
- @vite-pwa/sveltekit: 0.6.6 -> 1.1.0
- sass: 1.97.3 -> 1.101.0
- svelte: 5.17.3 -> 5.56.4
- svelte-check: 4.1.3 -> 4.7.2
- svelte-preprocess: 6.0.3 -> 6.0.5
- typescript: 5.8.3 -> 6.0.3 (going to give v7 time to mature before I try it)
- vite: 6.3.5 -> 8.1.4
- Added devDependencies: `@eslint/js` (new eslint has split this module out into a new package) and `globals` (as a result of new eslint architecture, we now have this as a direct dev dependency).
- ESLint: Migrate to flat config: I ran `npx sv add eslint` and modified the eslint.config.js it generated to line up better with our old one and work a bit better (i think) with our normal ts files. Also set `ecmaVersion` to `latest`, previously was `2020`.
- Moved `env.d.ts` to `src` directory (which is covered by svelte-kits include) so that we can drop the custom `include` in our `tsconfig.json` (which when out of sync with the generated tsconfig by svelte-kit can lead to headaches because it's not immediately obvious, so this change will eliminate that dev time footgun).
- Fixed lots of new (and probably old which may not have been applying with the old bad config) {ts,svelte} eslint rules, improving code quality.
# [4.1.1] - 2026-07-26T02:00:00Z
+2871 -2587
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -15,24 +15,24 @@
},
"devDependencies": {
"@eslint/js": "10.0.1",
"@sveltejs/adapter-node": "5.2.12",
"@sveltejs/kit": "2.21.0",
"@sveltejs/adapter-node": "5.5.7",
"@sveltejs/kit": "2.69.3",
"@types/papaparse": "5.3.15",
"@vite-pwa/sveltekit": "0.6.6",
"@vite-pwa/sveltekit": "1.1.0",
"eslint": "10.7.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-svelte": "3.20.0",
"globals": "17.7.0",
"prettier": "3.9.5",
"prettier-plugin-svelte": "4.1.1",
"sass": "1.97.3",
"svelte": "5.17.3",
"svelte-check": "4.1.3",
"sass": "1.101.0",
"svelte": "5.56.4",
"svelte-check": "4.7.2",
"svelte-eslint-parser": "1.8.0",
"svelte-preprocess": "6.0.3",
"typescript": "5.8.3",
"svelte-preprocess": "6.0.5",
"typescript": "6.0.3",
"typescript-eslint": "8.63.0",
"vite": "6.3.5"
"vite": "8.1.4"
},
"type": "module",
"dependencies": {
View File
+4 -4
View File
@@ -17,7 +17,7 @@
let { activity = undefined, onRemoved }: Props = $props();
let clickedActivity: Activity | undefined = $state();
let groupedActivities: { [index: string]: any } = $derived(
let groupedActivities: { [index: string]: Activity[] } = $derived(
getGroupedActivity(activity),
);
@@ -168,7 +168,7 @@
const a = activities?.sort(
(a, b) => getCreatedAtVis(b) - getCreatedAtVis(a),
);
let grouped: { [index: string]: any } = {};
let grouped: { [index: string]: Activity[] } = {};
if (a) {
for (let i = 0; i < a.length; i++) {
const activity = a[i];
@@ -231,10 +231,10 @@
<h2>Activity</h2>
{#if groupedActivities && Object.keys(groupedActivities).length > 0}
<ul>
{#each Object.keys(groupedActivities) as k}
{#each Object.keys(groupedActivities) as k (k)}
<h3>{k}</h3>
{#each groupedActivities[k] as a}
{#each groupedActivities[k] as a (a.id)}
{@const d = new Date(getCreatedAtVis(a))}
<li>
<button class="plain" onclick={() => openEditor(a)}>
+6 -18
View File
@@ -36,29 +36,17 @@
const pressedLetter = event.key.toLowerCase();
// Filter options that start with the pressed letter (ignoring case)
const filteredOptions = options.filter((o) =>
typeof o === "string"
? o.toLowerCase().startsWith(pressedLetter)
: o.value.toLowerCase().startsWith(pressedLetter),
);
// If there are filtered options, select the first one
let f: string | number | undefined;
if (filteredOptions.length > 0) {
f =
typeof filteredOptions[0] === "string"
? filteredOptions[0]
: filteredOptions[0].id;
}
// Find first button with text content starting with letter pressed and scroll it into view.
const btns = ulElement?.querySelectorAll("button");
if (btns) {
for (let i = 0; i < btns.length; i++) {
const btn = btns[i];
if (btn.textContent?.toLowerCase()?.startsWith(pressedLetter)) {
btn.scrollIntoView({ behavior: "smooth", block: "start" });
btn.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest",
});
break;
}
}
@@ -98,7 +86,7 @@
<Icon i="chevron" facing={open ? "up" : "down"} />
</button>
<ul bind:this={ulElement}>
{#each showActiveElementsInOptions ? options : options.filter( (o) => (typeof o === "string" ? o !== active : o.id !== active) ) as o}
{#each showActiveElementsInOptions ? options : options.filter( (o) => (typeof o === "string" ? o !== active : o.id !== active) ) as o (typeof o === "string" ? o : o.id)}
<li>
<button
class="plain"
+4 -4
View File
@@ -17,18 +17,18 @@
allowSelectMultipleFiles = false,
}: Props = $props();
let fileInput: HTMLInputElement = $state();
let dragEnterTarget: EventTarget | null = $state();
let fileInput: HTMLInputElement | undefined = $state();
let dragEnterTarget: EventTarget | null | undefined = $state();
let isDragOver = $state(false);
function importFile() {
fileInput.click();
fileInput?.click();
}
onMount(() => {
if (fileInput) {
fileInput.addEventListener("change", () => {
filesSelected(fileInput.files);
filesSelected(fileInput?.files);
});
}
});
+6 -4
View File
@@ -1,7 +1,9 @@
<script lang="ts">
import { ReqerError } from "./util/fetch";
interface Props {
pretty: string;
error: any;
error: unknown;
onRetry?: () => void | undefined;
}
@@ -12,10 +14,10 @@
<div>
<div class="error-text">
<strong>{pretty}</strong>
{#if error?.message}
{#if error instanceof Error && error.message}
<p>{error.message}</p>
{#if error.response?.data?.error}
<p>{error.response.data.error}</p>
{#if error instanceof ReqerError && error?.body?.error}
<p>{error.body.error}</p>
{/if}
{:else}
<p>{JSON.stringify(error)}</p>
+3 -3
View File
@@ -39,7 +39,7 @@
jobId = r.jobId;
step = "job-running";
startJobWatcher();
} catch (err: any) {
} catch (err) {
console.error("startSync failed!", err);
step = "errored";
jobFailError = ReqerError.getMsg(err, "Starting sync failed");
@@ -146,7 +146,7 @@
</h4>
<span>Job finished, but with errors:</span>
<ul>
{#each latestJobStatus?.errors as e}
{#each latestJobStatus?.errors as e (e)}
<li>{e}</li>
{/each}
</ul>
@@ -162,7 +162,7 @@
{/if}
{#if latestJobStatus?.errors && latestJobStatus?.errors?.length > 0}
<ul>
{#each latestJobStatus?.errors as e}
{#each latestJobStatus?.errors as e (e)}
<li>{e}</li>
{/each}
</ul>
+1 -1
View File
@@ -33,7 +33,7 @@
<span>{allCheckBox}</span>
</div>
{/if}
{#each options as o}
{#each options as o (o.id)}
<div>
<Checkbox
name={o.displayValue}
@@ -6,7 +6,7 @@
</script>
<div id="notifications">
{#each store.notifications as n}
{#each store.notifications as n (n.id)}
<div class={`${n.type} notif`}>
{#if n.type === "loading"}
<SpinnerTiny />
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from "$app/paths";
import type { PublicUser } from "@/types";
interface Props {
@@ -11,9 +12,9 @@
<div>
<h2>Users</h2>
<ul>
{#each users as user}
{#each users as user (user.id)}
<li title={user.username}>
<a href="/lists/{user.id}/{user.username}">
<a href={resolve(`/lists/${user.id}/${user.username}`)}>
<span>{user.username}</span>
</a>
</li>
+1
View File
@@ -49,6 +49,7 @@ export default function tooltip(node: HTMLElement, opts: ToolTipOptions) {
return {
update(opts: ToolTipOptions) {
text = opts.text;
pos = opts.pos || "left";
condition = opts.condition ?? true;
},
destroy() {
+1 -1
View File
@@ -37,7 +37,7 @@
{:then fts}
{#if fts?.length > 0}
<HorizontalList title="Followed Thoughts">
{#each fts as ft}
{#each fts as ft (ft.followedUser.id)}
<button
class={["thoughts-card plain", ft.thoughts ? "" : "no-thoughts"].join(
" ",
+26
View File
@@ -0,0 +1,26 @@
<script lang="ts">
import type { MediaGenre } from "@/types";
interface Props {
genres?: MediaGenre[];
}
let { genres }: Props = $props();
</script>
<div>
{#if genres && genres.length > 0}
{#each genres as g, i (g.id)}
<span>
{g.name}{i !== genres.length - 1 ? ", " : ""}
</span>
{/each}
{:else}
<!-- Generic "unknown" text, since this component
is also used for displaying Game Modes. -->
<span>Unknown</span>
{/if}
</div>
<style lang="scss">
</style>
+3 -3
View File
@@ -164,7 +164,7 @@
</svg>
</a>
{:else if i === "Steam"}
<a aria-label={i} {href} target="_blank">
<a aria-label={i} {href} rel="external" target="_blank">
<svg
width="30"
height="30"
@@ -177,7 +177,7 @@
</svg>
</a>
{:else if i === "GOG"}
<a aria-label={i} {href} target="_blank">
<a aria-label={i} {href} rel="external" target="_blank">
<svg
width="30"
height="30"
@@ -190,7 +190,7 @@
</svg>
</a>
{:else if i === "Itch"}
<a aria-label={i} {href} target="_blank">
<a aria-label={i} {href} rel="external" target="_blank">
<svg
width="30"
height="30"
+4 -2
View File
@@ -13,14 +13,16 @@
{#if providers?.length > 0}
<div class="streaming-providers">
{#each providers as provider}
{#each providers as provider (provider.name)}
<ProviderIcon i={provider.name} href={provider.link} wh={40} />
{/each}
{#if fullListLink}
<!-- The fullListLink is important for TMDB data, we always show it
as "JustWatch" (set in component prop) because that data requires
attribution! but also it helps support tmdb in some way. -->
<a href={fullListLink} target="_blank">{fullListLinkText}</a>
<a href={fullListLink} rel="external" target="_blank">
{fullListLinkText}
</a>
{/if}
</div>
{/if}
+1 -1
View File
@@ -12,7 +12,7 @@
{#if similar?.length > 0}
<HorizontalList title="Similar">
{#each similar as content, i}
{#each similar as content, i (content.ids)}
<Poster media={content} small={true} bind:watched={similar[i].watched} />
{/each}
</HorizontalList>
+1 -1
View File
@@ -23,7 +23,7 @@
<span class="title-container">
<span class="title">
{#if homepage}
<a href={homepage} target="_blank">{titleSafe}</a>
<a href={homepage} rel="external" target="_blank">{titleSafe}</a>
{:else}
<span class="t">{titleSafe}</span>
{/if}
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from "$app/paths";
import type { TMDBContentCreditsCrew } from "@/types";
interface Props {
@@ -9,9 +10,9 @@
</script>
<div class="creators">
{#each topCrew as crew}
{#each topCrew as crew (crew.id)}
<div>
<a href={`/person/${crew.id}`}>{crew.name}</a>
<a href={resolve(`/person/${crew.id}`)}>{crew.name}</a>
<span>{crew.job}</span>
</div>
{/each}
+1 -1
View File
@@ -8,7 +8,7 @@
let { embed, closed }: Props = $props();
let modalDiv: HTMLDivElement = $state();
let modalDiv: HTMLDivElement | undefined = $state();
onMount(() => {
// For better experience on keyboard.
+2 -1
View File
@@ -6,6 +6,7 @@
import { clearWatcharrData } from ".";
import { goto } from "$app/navigation";
import { req } from "../util/api";
import { resolve } from "$app/paths";
interface Props {
onClose: () => void;
@@ -34,7 +35,7 @@
function logout() {
clearWatcharrData();
goto("/login?noAuto=1");
goto(resolve("/login?noAuto=1"));
}
function proxyLogout() {
+6 -5
View File
@@ -8,6 +8,7 @@
import { clearWatcharrData } from "../logout";
import { notify } from "../util/notify";
import AboutModal from "./AboutModal.svelte";
import { resolve } from "$app/paths";
let user = $derived(store.userInfo);
let proxyUserLogoutShown = $state(false);
@@ -20,23 +21,23 @@
return;
}
clearWatcharrData();
goto("/login");
goto(resolve("/login"));
}
function profile() {
goto("/profile");
goto(resolve("/profile"));
}
function serverSettings() {
goto("/server");
goto(resolve("/server"));
}
function userManagement() {
goto("/manage_users");
goto(resolve("/manage_users"));
}
function requestManagement() {
goto("/arr_requests");
goto(resolve("/arr_requests"));
}
function shareWatchedList() {
+6 -3
View File
@@ -1,9 +1,10 @@
<script lang="ts">
import { store } from "@/store.svelte";
import Menu from "../Menu.svelte";
import { resolve } from "$app/paths";
interface Props {
close: () => {};
close: () => void;
}
let { close }: Props = $props();
@@ -13,9 +14,11 @@
{#if store.follows?.length > 0}
<h4 class="norm sm-caps">following</h4>
<div class="list">
{#each store.follows as f}
{#each store.follows as f (f.followedUser.id)}
<a
href="/lists/{f.followedUser.id}/{f.followedUser.username}"
href={resolve(
`/lists/${f.followedUser.id}/${f.followedUser.username}`,
)}
onclick={() => close()}
>
{f.followedUser.username}
+16 -6
View File
@@ -21,41 +21,51 @@
}
store.activeSort = [type, mode];
}
function getDirectionClass(sort: string): string {
if (store.activeSort[0] !== sort) {
return "";
}
if (store.activeSort[1]) {
return store.activeSort[1].toLowerCase();
}
return "";
}
</script>
<Menu conf={{ width: "180px", right: "90px", arrowLeft: "21px" }}>
<button
class={`plain ${store.activeSort[0] == "DATEADDED" ? store.activeSort[1].toLowerCase() : ""}`}
class={`plain ${getDirectionClass("DATEADDED")}`}
onclick={() => sortClicked("DATEADDED")}
>
Date Added
</button>
<button
class={`plain ${store.activeSort[0] == "LASTCHANGED" ? store.activeSort[1].toLowerCase() : ""}`}
class={`plain ${getDirectionClass("LASTCHANGED")}`}
onclick={() => sortClicked("LASTCHANGED")}
>
Last Changed
</button>
<button
class={`plain ${store.activeSort[0] == "LASTFIN" ? store.activeSort[1].toLowerCase() : ""}`}
class={`plain ${getDirectionClass("LASTFIN")}`}
onclick={() => sortClicked("LASTFIN")}
>
Last Finished
</button>
<button
class={`plain ${store.activeSort[0] == "RATING" ? store.activeSort[1].toLowerCase() : ""}`}
class={`plain ${getDirectionClass("RATING")}`}
onclick={() => sortClicked("RATING")}
>
Rating
</button>
<button
class={`plain ${store.activeSort[0] == "ALPHA" ? store.activeSort[1].toLowerCase() : ""}`}
class={`plain ${getDirectionClass("ALPHA")}`}
onclick={() => sortClicked("ALPHA")}
>
Alphabetical
</button>
<button
class={`plain ${store.activeSort[0] == "DATERELEASED" ? store.activeSort[1].toLowerCase() : ""}`}
class={`plain ${getDirectionClass("DATERELEASED")}`}
onclick={() => sortClicked("DATERELEASED")}
>
Release Date
+6 -3
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import {
addClassToParent,
calculateTransformOrigin,
@@ -24,7 +25,9 @@
const poster = path
? `https://image.tmdb.org/t/p/w300_and_h450_bestv2${path}`
: undefined;
const link = id ? `/person/${id}` : undefined;
const link: `/person/${number}` | undefined = id
? `/person/${id}`
: undefined;
</script>
<!-- Quick fix to ignore error, should be fixed -->
@@ -33,7 +36,7 @@
onmouseenter={(e) => calculateTransformOrigin(e)}
onfocusin={(e) => calculateTransformOrigin(e)}
onclick={() => {
if (link) goto(link);
if (link) goto(resolve(link));
}}
onkeypress={() => console.log("on kpress")}
>
@@ -58,7 +61,7 @@
<div class="inner">
<h2>
{#if link}
<a data-sveltekit-preload-data="tap" href={link}>
<a data-sveltekit-preload-data="tap" href={resolve(link)}>
{name}
</a>
{:else}
+13 -6
View File
@@ -21,6 +21,7 @@
import { buildExtraDetails } from "./lib";
import { decode } from "blurhash";
import WatchedDeleteModal from "../watched/WatchedDeleteModal.svelte";
import { resolve } from "$app/paths";
interface Props {
media: Media;
@@ -139,7 +140,9 @@
return `https://images.igdb.com/igdb/image/upload/t_cover_big/${media.extPosterPath}.jpg`;
}
});
const link = $derived(meta?.id ? `/${meta.type}/${meta.id}` : undefined);
const link = $derived<`${`/${SupportedMedia}/${string}`}` | undefined>(
meta?.id ? `/${meta.type}/${meta.id}` : undefined,
);
const year = $derived(
media.releaseDate ? new Date(media.releaseDate).getFullYear() : undefined,
);
@@ -211,7 +214,7 @@
return;
}
if (link) {
goto(link);
goto(resolve(link));
}
}
}
@@ -342,10 +345,10 @@
loading="lazy"
src={poster}
alt=""
onload={(e) => {
onload={() => {
posterImgLoaded = 1;
}}
onerror={(e) => {
onerror={() => {
posterImgLoaded = -1;
}}
/>
@@ -362,7 +365,7 @@
e.preventDefault();
return;
}
if (posterActive && link) goto(link);
if (posterActive && link) goto(resolve(link));
}}
onkeyup={handleInnerKeyUp}
id="ilikemoviessueme"
@@ -370,7 +373,11 @@
role="button"
tabindex="-1"
>
<a data-sveltekit-preload-data="tap" href={link} class="small-scrollbar">
<a
data-sveltekit-preload-data="tap"
href={link ? resolve(link) : undefined}
class="small-scrollbar"
>
<h2>
{media.name}
{#if year}
+1 -1
View File
@@ -8,7 +8,7 @@
let { type = "wrapped", children }: Props = $props();
let ulEl: HTMLUListElement = $state();
let ulEl: HTMLUListElement | undefined = $state();
onMount(() => {
if (ulEl) {
+4 -3
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { store } from "@/store.svelte";
import tooltip from "../actions/tooltip";
import { RatingStep, RatingSystem } from "@/types";
import { RatingSystem } from "@/types";
import Icon from "../Icon.svelte";
import { toShowableRating, toWhichThumb } from "../rating/helpers";
@@ -49,7 +49,7 @@
ev.stopPropagation();
ratingsShown = !ratingsShown;
}}
onmouseleave={(ev) => {
onmouseleave={() => {
ratingsShown = false;
}}
use:tooltip={{
@@ -88,6 +88,7 @@
{toShowableRating(rating)}
{/if}
{:else if minimal}
<!-- eslint-disable-next-line svelte/no-useless-mustaches -->
{""}
{:else if disableInteraction}
Unrated
@@ -139,7 +140,7 @@
store.userSettings?.ratingSystem == RatingSystem.OutOf5
? [5, 4, 3, 2, 1]
: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]}
{#each stars as v}
{#each stars as v (v)}
<!-- svelte-ignore node_invalid_placement_ssr -->
<button
class="plain{rating === v ? ' active' : ''}"
+4 -4
View File
@@ -36,7 +36,7 @@
ev.stopPropagation();
statusesShown = !statusesShown;
}}
onmouseleave={(ev) => {
onmouseleave={() => {
statusesShown = false;
}}
use:tooltip={{
@@ -58,13 +58,13 @@
direction,
].join(" ")}
>
{#each Object.entries(watchedStatuses) as [statusName, icon]}
{#each Object.entries(watchedStatuses) as [statusName, icon] (statusName)}
<!-- svelte-ignore node_invalid_placement_ssr -->
<button
class="plain{status && status !== statusName ? ' not-active' : ''}"
onclick={() => handleStatusClick(statusName)}
onclick={() => handleStatusClick(statusName as WatchedStatus)}
use:tooltip={{
text: toUnderstandableStatus(statusName, isForGame),
text: toUnderstandableStatus(statusName as WatchedStatus, isForGame),
}}
>
<Icon i={icon} />
+3 -7
View File
@@ -16,7 +16,6 @@
let { rating, onChange }: Props = $props();
let hoveredRating: number | undefined = $state();
let shownRating: number | undefined = $state();
let shownPerc: number | undefined = $state();
let ratingContainer: HTMLDivElement | undefined = $state();
let ratingWrapEl: HTMLDivElement | undefined = $state();
@@ -217,7 +216,6 @@
run(() => {
if (hoveredRating !== undefined && hoveredRating > 0) {
console.debug("showRatingCaller: We have a hoveredRating.");
shownRating = hoveredRating;
showRating(
Math.round(
(hoveredRating * 100) /
@@ -226,11 +224,9 @@
);
} else if (rating !== undefined) {
console.debug("showRatingCaller: We have a rating.");
shownRating = rating;
showRating(Math.round((rating * 100) / 10));
} else {
console.debug("showRatingCaller: We have nothing.");
shownRating = undefined;
showRating(0);
}
});
@@ -383,7 +379,7 @@ shownPerc: {shownPerc}<br /> -->
class="rating the-normal-one"
tabindex="-1"
>
{#each stars as _}
{#each stars as _ (_)}
<button class="plain" tabindex="-1">*</button>
{/each}
</div>
@@ -393,13 +389,13 @@ shownPerc: {shownPerc}<br /> -->
class="rating the-highlight-one"
tabindex="-1"
>
{#each stars as _}
{#each stars as _ (_)}
<button class="plain lit" tabindex="-1">*</button>
{/each}
</div>
<!-- Hidden stars, just to keep correct layout since the two above are abolute. -->
<div class="rating the-hidden-one-for-layout-reasons" tabindex="-1">
{#each stars as _}
{#each stars as _ (_)}
<button
class="plain"
style="opacity: 0; pointer-events: none;"
-1
View File
@@ -1,6 +1,5 @@
import { store } from "@/store.svelte";
import { RatingStep, RatingSystem } from "@/types";
import { get } from "svelte/store";
/**
* Used for scaling users 'actual' rating we store in db
+2 -1
View File
@@ -60,10 +60,11 @@
getStatus();
}
}
} catch (err: any) {
} catch (err) {
if (
ReqerError.withBody(err) &&
err.response?.status === 404 &&
err.hasErrorInBody() &&
err.body.error === "request deleted"
) {
return;
-5
View File
@@ -28,13 +28,11 @@
let servarrs: RadarrSettingsPublicResponseResult[] | undefined = $state();
let selectedServarrIndex: number = $state(0);
let inputsDisabled = true;
let selectedServerCfg: RadarrTestResponse | undefined = $state();
let addRequestRunning = $state(false);
async function getServers() {
try {
inputsDisabled = true;
const r = await req.get<RadarrSettingsPublicResponseResult[]>("/arr/rad");
if (r?.length > 0) {
servarrs = r;
@@ -42,7 +40,6 @@
} else {
notify({ text: "No servers found", type: "error" });
}
inputsDisabled = false;
processOriginalRequest();
} catch (err) {
console.error("Failed to get servers!", err);
@@ -52,10 +49,8 @@
async function getConfig(name: string) {
try {
inputsDisabled = true;
const r = await req.get<RadarrTestResponse>(`/arr/rad/config/${name}`);
selectedServerCfg = r;
inputsDisabled = false;
} catch (err) {
console.error("Failed to get config!", err);
notify({ text: "Failed to load config", type: "error" });
-5
View File
@@ -30,7 +30,6 @@
let servarrs: SonarrSettingsPublicResponseResult[] | undefined = $state();
let selectedServarrIndex: number = $state(0);
let inputsDisabled = true;
let selectedServerCfg: SonarrTestResponse | undefined = $state();
let seasonItems: ListBoxItem[] = $state(
content.seasons
@@ -50,7 +49,6 @@
async function getServers() {
try {
inputsDisabled = true;
const r = await req.get<SonarrSettingsPublicResponseResult[]>("/arr/son");
if (r.length > 0) {
servarrs = r;
@@ -58,7 +56,6 @@
} else {
notify({ text: "No servers found", type: "error" });
}
inputsDisabled = false;
processOriginalRequest();
} catch (err) {
console.error("Failed to get servers!", err);
@@ -68,10 +65,8 @@
async function getConfig(name: string) {
try {
inputsDisabled = true;
const r = await req.get<SonarrTestResponse>(`/arr/son/config/${name}`);
selectedServerCfg = r;
inputsDisabled = false;
} catch (err) {
console.error("Failed to get config!", err);
notify({ text: "Failed to load config", type: "error" });
+2 -2
View File
@@ -132,7 +132,7 @@
<div class="ctr">
<ul class="seasons">
{#each seasons as season}
{#each seasons as season (season.number)}
<button
class="plain"
class:active={activeSeason === season.number}
@@ -216,7 +216,7 @@
</div>
{#if season?.episodes?.length > 0}
<ul>
{#each season.episodes as ep}
{#each season.episodes as ep (ep.id)}
<SeasonsListEpisode {ep} {watchedItem} />
{/each}
</ul>
+1 -1
View File
@@ -125,7 +125,7 @@
</div>
{#if watchedItem}
<div class="status-rating-ctr">
<div class="rating" style={"width: 45px"}>
<div class="rating" style="width: 45px">
<PosterRating
rating={we?.rating}
btnTooltip={`Episode ${ep.episode_number} Rating`}
+5 -1
View File
@@ -1,11 +1,15 @@
<script lang="ts">
import type { ResolvedPathname } from "$app/types";
import tooltip from "../actions/tooltip";
interface Props {
name: string;
value: string | number;
large?: boolean;
href?: string | undefined;
/**
* **NOTE:** Make sure to use resolve() when passing in the href!
*/
href?: ResolvedPathname;
disc?: string | undefined;
}
+1 -1
View File
@@ -56,7 +56,7 @@
pos: "bot",
}}
>
<Icon i={"tag"} wh={19} />
<Icon i="tag" wh={19} />
</button>
{#if menuOpen}
+3 -17
View File
@@ -1,6 +1,4 @@
<script lang="ts">
import { run } from "svelte/legacy";
import type { Tag } from "@/types";
interface Props {
@@ -9,24 +7,12 @@
}
let { tag, onClick = undefined! }: Props = $props();
let tagBtn: HTMLButtonElement = $state();
run(() => {
if (tagBtn) {
if (tag.color) {
tagBtn.style.color = tag.color;
}
if (tag.bgColor) {
tagBtn.style.background = tag.bgColor;
}
}
});
</script>
<button
bind:this={tagBtn}
class={`plain`}
class="plain"
style:color={tag.color}
style:background={tag.bgColor}
onclick={() => {
if (typeof onClick === "function") {
onClick();
+1 -1
View File
@@ -68,7 +68,7 @@
>
{/if}
<div class="list">
{#each allTags as t}
{#each allTags as t (t.id)}
{@const isSelected = selectedTags
? selectedTags.find((tag) => tag.id === t.id)
? true
+1 -1
View File
@@ -139,7 +139,7 @@ export async function updateWatched(
// Add new watched item
notify({ id: nid, text: `Adding`, type: "loading" });
let reqBody: WatchedAddRequest = {
const reqBody: WatchedAddRequest = {
contentType: opts.contentType,
status: opts.status,
rating: opts.rating,
+45 -24
View File
@@ -1,13 +1,20 @@
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { clearWatcharrData } from "../logout";
import { notify } from "./notify";
type ReqerParams = any;
type ReqerParams = object;
/**
* Request config, extending upon base RequestInit.
*/
export interface ReqerConfig extends RequestInit {
export interface ReqerConfig extends Omit<RequestInit, "body"> {
/**
* Request body.
* See `prepareRequestBody()` for how this property is processed before
* being attached to the request.
*/
body?: unknown;
/**
* URL Parameters.
*
@@ -21,7 +28,7 @@ type ReqerConfigWithoutMethod = Omit<ReqerConfig, "method">;
export class ReqerError extends Error {
constructor(
public message: string,
public body?: any,
public body?: unknown,
public response?: Response,
) {
super(message);
@@ -31,7 +38,7 @@ export class ReqerError extends Error {
/**
* If err is a ReqerError.
*/
static isReqerError(err: any) {
static isReqerError(err: unknown) {
return err instanceof ReqerError;
}
@@ -39,9 +46,21 @@ export class ReqerError extends Error {
* If err is a ReqerError and has a `body`.
*/
static withBody(
err: any,
err: unknown,
): err is ReqerError & Required<Pick<ReqerError, "body">> {
return this.isReqerError(err) && err.body;
return this.isReqerError(err) && Boolean(err.body);
}
/**
* If `body` is an object and contains a standard Watcharr `error` property
* containing a string.
*/
hasErrorInBody(): this is this & { body: { error: string } } {
return (
this.body instanceof Object &&
"error" in this.body &&
typeof this.body.error === "string"
);
}
/**
@@ -54,7 +73,7 @@ export class ReqerError extends Error {
* `act` should be the action that failed. It will be combined with the
* error message to create one readable string of `action: err`.
*/
static getMsg(err: any, act: string): string {
static getMsg(err: unknown, act: string): string {
// If response body has `error` property, is is a standard error
// object that the Watcharr server returns. We don't want to return
// a raw `body` if there is no expected `error` property incase it is
@@ -62,9 +81,9 @@ export class ReqerError extends Error {
// error whenever possible, debugging errors are always in console),
// if there is no `error`, we try the `err.message`.
let msg = "You've encountered an extremely unguarded error!";
if (ReqerError.withBody(err) && err.body.error) {
if (ReqerError.withBody(err) && err.hasErrorInBody()) {
msg = err.body.error;
} else if (err.message) {
} else if (err instanceof Error && err.message) {
msg = err.message;
}
return `${act}: ${msg}`;
@@ -122,7 +141,7 @@ export class Reqer {
private buildUrlPath(p: string): string {
// Removes any slashes or "." from start of path.
return p.replace(/^[.\/\\]+/, "");
return p.replace(/^[./\\]+/, "");
}
private buildUrl(p: string, params?: ReqerParams): URL {
@@ -136,14 +155,12 @@ export class Reqer {
this.buildBaseUrl(this.baseUrl),
);
if (params && typeof params === "object") {
for (const k in params) {
if (!Object.hasOwn(params, k)) continue;
const el = params[k];
if (el) {
for (const [k, val] of Object.entries(params)) {
if (k && val) {
// We use append on url.searchParams instead of
// overwriting it with a new object incase it has any
// existing params parsed from the `p`ath.
url.searchParams.append(k, String(el));
url.searchParams.append(k, String(val));
}
}
}
@@ -155,7 +172,7 @@ export class Reqer {
}
private prepareRequestBody(
data: any,
data: unknown,
headers: Headers,
): BodyInit | undefined {
if (!data) {
@@ -186,7 +203,11 @@ export class Reqer {
private async parseResponseBody(res: Response) {
const contentType = res.headers.get("Content-Type");
if (!contentType) {
throw new ReqerError("response has no content-type", res);
// Fallback, just return text.
console.warn(
"Reqer->parseResponseBody: No content-type in response. Returning text.",
);
return await res.text();
}
if (contentType.includes("application/json")) {
return await res.json();
@@ -204,7 +225,7 @@ export class Reqer {
const token = localStorage.getItem("token");
if (!token) {
console.error("No token, going to login.");
goto("/login?again=1");
goto(resolve("/login?again=1"));
throw new ReqerError("No auth token found");
}
headers.append("Authorization", token);
@@ -218,7 +239,7 @@ export class Reqer {
headers,
});
let resBody = await this.parseResponseBody(res);
const resBody = await this.parseResponseBody(res);
if (!res.ok) {
throw new ReqerError(
@@ -240,7 +261,7 @@ export class Reqer {
console.error("Recieved 401 response, going to login.");
notify({ text: "Request Authorization Failed!", type: "error" });
clearWatcharrData();
goto("/login?again=1");
goto(resolve("/login?again=1"));
}
throw err;
} else if (err instanceof Error) {
@@ -272,7 +293,7 @@ export class Reqer {
*/
async post<T>(
p: string,
data?: any,
data?: unknown,
cfg?: Omit<ReqerConfigWithoutMethod, "body">,
): Promise<T> {
return (await this.do<T>(p, { ...cfg, method: "POST", body: data })).body;
@@ -283,7 +304,7 @@ export class Reqer {
*/
async postWhole<T>(
p: string,
data?: any,
data?: unknown,
cfg?: Omit<ReqerConfigWithoutMethod, "body">,
): Promise<ReqerResponse<T>> {
return await this.do<T>(p, { ...cfg, method: "POST", body: data });
@@ -294,7 +315,7 @@ export class Reqer {
*/
async put<T>(
p: string,
data?: any,
data?: unknown,
cfg?: ReqerConfigWithoutMethod,
): Promise<T> {
return (await this.do<T>(p, { ...cfg, method: "PUT", body: data })).body;
@@ -305,7 +326,7 @@ export class Reqer {
*/
async putWhole<T>(
p: string,
data?: any,
data?: unknown,
cfg?: ReqerConfigWithoutMethod,
): Promise<ReqerResponse<T>> {
return await this.do<T>(p, { ...cfg, method: "PUT", body: data });
+1 -4
View File
@@ -1,13 +1,9 @@
import {
UserPermission,
type Icon,
type MediaType,
type TMDBContentCreditsCrew,
type TokenClaims,
type Watched,
type WatchedStatus,
type WatchedEpisode,
type WatchedSeason,
} from "@/types";
export const watchedStatuses: {
@@ -174,6 +170,7 @@ export function parseTokenPayload(): TokenClaims | undefined {
if (!token) return;
return JSON.parse(atob(token.split(".")[1])) as TokenClaims;
} catch (err) {
console.error("parseTokenPayload: Failed.", err);
return;
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ export interface ToolTipOptions {
* loading from causing extra data requests, etc.
*/
export default function infScroll(opts: ToolTipOptions) {
let { threshold = 150, callback } = opts;
const { threshold = 150, callback } = opts;
// Store current pathname at point of infScroll
// initialization, this ensures we have a point
+14 -11
View File
@@ -23,7 +23,7 @@ export default function paginatedLoader<T, U>(
page: number;
pageMax: number;
reqLoading: boolean;
reqLoadError: Error | undefined;
reqLoadError: unknown | undefined;
} = $state({
data: [],
meta: undefined,
@@ -89,9 +89,15 @@ export default function paginatedLoader<T, U>(
}
state.reqLoading = true;
reqController = new AbortController();
/**
* Keep a "local" copy of the AbortController that will always exist in
* this scope, to avoid race conditions where a future request overrides
* the one for this scope that we want to check (eg: the `catch` below).
*/
const locReqController = new AbortController();
reqController = locReqController;
try {
const resp = await fn(reqController.signal);
const resp = await fn(locReqController.signal);
if (!resp) {
state.reqLoading = false;
console.error(
@@ -111,16 +117,13 @@ export default function paginatedLoader<T, U>(
return;
}
state.data.push(...resp.results);
state.data = state.data;
// state.data = state.data;
state.meta = resp.meta;
} catch (err: any) {
if (err?.code === "ERR_CANCELED") {
} catch (err) {
if (locReqController.signal.aborted) {
console.warn("loadWatchedList: Cancelled, not showing error.");
// If request cancelled (likely by us aborting), then return
// here to avoid updating reqLoading state to false below.
// This fixes the case where we abort a request and start the
// next one before this one throws, which sets reqLoading to
// false for our next request (race condition).
// If request cancelled (by us aborting), then we want to ignore
// this error and let the new request do its thing.
return;
} else {
console.error("loadWatchedList: failed!", err);
+3 -2
View File
@@ -18,6 +18,7 @@ interface PlexPin {
//
function uuidv4() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c: any) =>
(
c ^
@@ -109,7 +110,7 @@ export async function plexPinPoll(
const doPoll = async () => {
try {
console.debug("plexPinPoll");
const r = await plexTvReq.get<any>(`/pins/${pin.id}`, {
const r = await plexTvReq.get<{ authToken?: string }>(`/pins/${pin.id}`, {
headers: { ...headers, code: pin.code },
});
if (r?.authToken) {
@@ -129,7 +130,7 @@ export async function plexPinPoll(
}
async function getPlexPin(headers: Record<string, string>): Promise<PlexPin> {
const r = await plexTvReq.post<any>(`/pins?strong=true`, undefined, {
const r = await plexTvReq.post<PlexPin>(`/pins?strong=true`, undefined, {
headers: headers,
});
console.debug("getPlexPin:", r);
+9 -7
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { afterNavigate, goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/state";
import Error from "@/lib/Error.svelte";
import Icon from "@/lib/Icon.svelte";
@@ -22,6 +23,7 @@
UserSettings,
} from "@/types";
import { onMount } from "svelte";
import { SvelteURLSearchParams } from "svelte/reactivity";
interface Props {
children?: import("svelte").Snippet;
}
@@ -41,7 +43,7 @@
function handleProfileClick() {
if (!localStorage.getItem("token")) {
goto("/login");
goto(resolve("/login"));
} else {
closeAllSubMenus("sub");
subMenuShown = !subMenuShown;
@@ -78,7 +80,7 @@
const query = target?.value.trim();
if (!query) return;
const currentSearchType = page.url.searchParams.get("type");
const searchParams = new URLSearchParams({
const searchParams = new SvelteURLSearchParams({
query: encodeURIComponent(query),
preferMyList: "true",
});
@@ -92,7 +94,7 @@
// Using autofocus seems to work. Disables after goto runs.
// https://github.com/sbondCo/Watcharr/issues/169
target.autofocus = true;
goto(`/search?${searchParams.toString()}`).then(() => {
goto(resolve(`/search?${searchParams.toString()}`)).then(() => {
// Use mainSearchEl if nav not split, otherwise use ev target.
if (!document.body.classList.contains("split-nav") && mainSearchEl) {
mainSearchEl.focus();
@@ -110,7 +112,7 @@
async function getInitialData() {
if (!localStorage.getItem("token")) {
console.warn("getInitialData: No token found, redirecting to login!");
goto("/login?again=1");
goto(resolve("/login?again=1"));
return;
}
const [u, s, f, fo, ts] = await Promise.all([
@@ -238,7 +240,7 @@
<nav bind:this={navEl}>
<div class="wrapper">
<div class="left-side">
<a href="/">
<a href={resolve("/")}>
<span class="large">Watcharr</span>
<span class="small">W</span>
</a>
@@ -330,7 +332,7 @@
{#if tagMenuShown}
<TagMenu
onTagClick={(tag) => {
goto(`/tag/${tag.id}`);
goto(resolve(`/tag/${tag.id}`));
tagMenuShown = false;
}}
showManageBtn={true}
@@ -338,7 +340,7 @@
{/if}
<button
class="plain other discover"
onclick={() => goto("/discover")}
onclick={() => goto(resolve("/discover"))}
use:tooltip={{ text: "Discover", pos: "bot" }}
>
<Icon i="compass" wh={26} />
+16 -10
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import Error from "@/lib/Error.svelte";
import Icon from "@/lib/Icon.svelte";
import Poster from "@/lib/poster/Poster.svelte";
@@ -17,7 +18,7 @@
let nextLoadParams: {
page: number;
[x: string]: any;
[x: string]: unknown;
} = $derived({
page: dataLoader.state.page + 1,
...store.sortAndFiltersForQueryParams,
@@ -72,15 +73,20 @@
<title>Watched List</title>
</svelte:head>
<!-- <span style="position: fixed; top: 80px; background-color: white; z-index: 60;"
><b>listPage</b>: {dataLoader.state.page} listPageMax: {dataLoader.state
.pageMax} listLoading:
{dataLoader.state.reqLoading}
<!-- <span
style="position: fixed; top: 80px; background-color: white; color: black; z-index: 60;"
>
<b>listPage</b>: {dataLoader.state.page}
listPageMax: {dataLoader.state.pageMax}
listLoading: {dataLoader.state.reqLoading}
<b>sort:</b>
{JSON.stringify(store.activeSort)} <b>filter:</b>
{JSON.stringify(store.activeFilters)} <b>queryp:</b>
{JSON.stringify(store.sortAndFiltersForQueryParams)}</span
> -->
{JSON.stringify(store.activeSort)}
<b>filter:</b>
{JSON.stringify(store.activeFilters)}
<b>queryp:</b>
{JSON.stringify(store.sortAndFiltersForQueryParams)}
paginatedLoader.state.meta: {JSON.stringify(dataLoader.state.meta)}
</span> -->
<PosterList>
{#if dataLoader.state.data?.length > 0}
@@ -102,7 +108,7 @@
searching for something you would like to add.
</h4>
{#if !store.hasActiveFilters}
<button onclick={() => goto("/import")}>Import</button>
<button onclick={() => goto(resolve("/import"))}>Import</button>
{/if}
{#if store.hasActiveFilters}
<button onclick={() => clearActiveFilters()}>Clear Filters</button>
+5 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from "$app/paths";
import Error from "@/lib/Error.svelte";
import Spinner from "@/lib/Spinner.svelte";
import RequestMovie from "@/lib/request/RequestMovie.svelte";
@@ -69,7 +70,7 @@
<Spinner />
{:then}
<div class="request-container">
{#each allRequests as r}
{#each allRequests as r (r.id)}
<div class={`request ${r.content.type}`}>
<div class="poster">
<img
@@ -89,7 +90,9 @@
<h2 class="norm">
<a
data-sveltekit-preload-data="tap"
href={`/${r.content.type}/${r.content.tmdbId}`}
href={r.content.type !== "tv_episode"
? resolve(`/${r.content.type}/${r.content.tmdbId}`)
: undefined}
class="plain"
>
{r.content.title}
+2 -1
View File
@@ -23,6 +23,7 @@
import Error from "@/lib/Error.svelte";
import PersonPoster from "@/lib/poster/PersonPoster.svelte";
import FilterDropDown from "./FilterDropDown.svelte";
import { resolve } from "$app/paths";
const scroll = infScroll({ callback: onScrollToBottom });
const dataLoader = paginatedLoader<Media, undefined>(load);
@@ -73,7 +74,7 @@
}
// Running the goto will cause afterNavigate hook to be called,
// which will run a fresh search, so nothing else to do here.
goto(`?${curLocation.searchParams.toString()}`);
goto(resolve(`/discover?${curLocation.searchParams.toString()}`));
}
onMount(() => {
+7 -37
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import Spinner from "@/lib/Spinner.svelte";
import HorizontalList from "@/lib/HorizontalList.svelte";
import { type Media, type WatchedStatus } from "@/types";
import Activity from "@/lib/Activity.svelte";
import Title from "@/lib/content/Title.svelte";
@@ -15,15 +14,16 @@
import ViewTrailerButton from "@/lib/content/ViewTrailerButton.svelte";
import ProvidersList from "@/lib/content/ProvidersList.svelte";
import PosterImage from "@/lib/content/PosterImage.svelte";
import Poster from "@/lib/poster/Poster.svelte";
import ExpandableText from "@/lib/content/ExpandableText.svelte";
import WatchedDeleteBtn from "@/lib/content/WatchedDeleteBtn.svelte";
import { activityRemovedHook } from "@/lib/activity.js";
import Genres from "@/lib/content/Genres.svelte";
import SimilarContent from "@/lib/content/SimilarContent.svelte";
let { data } = $props();
let game: Media | undefined = $state();
let pageError: Error | undefined = $state();
let pageError: unknown | undefined = $state();
let backdropSrc = $derived.by(() => {
const base = "https://images.igdb.com/igdb/image/upload/t_1080p/";
@@ -43,7 +43,7 @@
return;
}
game = await req.get<Media>(`/game/${data.gameId}`);
} catch (err: any) {
} catch (err) {
game = undefined;
pageError = err;
}
@@ -114,31 +114,9 @@
/>
<span class="quick-info">
{#if game.genres && game.genres?.length > 0}
<div>
{#each game.genres as g, i}
<span
>{g.name}{i !== game.genres.length - 1 ? ", " : ""}</span
>
{/each}
</div>
{:else}
<span>Unknown Genres</span>
{/if}
<Genres genres={game.genres} />
<span></span>
<div>
{#if game.gameModes && game.gameModes?.length > 0}
{#each game.gameModes as g, i}
<span
>{g.name}{i !== game.gameModes.length - 1
? ", "
: ""}</span
>
{/each}
{:else}
<span>Unknown Game Modes</span>
{/if}
</div>
<Genres genres={game.gameModes} />
</span>
<ExpandableText text={game.summary} style="margin-bottom: 18px;" />
@@ -200,15 +178,7 @@
{/if}
{#if game.similar && game.similar?.length > 0}
<HorizontalList title="Similar">
{#each game.similar as g, i}
<Poster
media={g}
bind:watched={game.similar[i].watched}
small={true}
/>
{/each}
</HorizontalList>
<SimilarContent similar={game.similar} />
{/if}
{#if game.watched}
+16 -26
View File
@@ -25,8 +25,8 @@
TodoMoviesMovie,
} from "@/types";
import Icon from "@/lib/Icon.svelte";
import { resolve } from "$app/paths";
let isDragOver = $state(false);
let isLoading = $state(false);
function processFiles(
@@ -42,7 +42,6 @@
text: "File not found in dropped items. Please try again or refresh.",
time: 6000,
});
isDragOver = false;
return;
}
isLoading = true;
@@ -61,7 +60,6 @@
text: "Text list export must be a .txt file!",
});
isLoading = false;
isDragOver = false;
return;
}
if ((type === "tmdb" || type === "imdb") && file.type !== "text/csv") {
@@ -70,7 +68,6 @@
text: `${type} export must be a .csv file!`,
});
isLoading = false;
isDragOver = false;
return;
}
const r = new FileReader();
@@ -82,7 +79,7 @@
data: r.result.toString(),
type,
};
goto("/import/process");
goto(resolve("/import/process"));
}
},
false,
@@ -139,7 +136,6 @@
text: "File not found in dropped items. Please try again or refresh.",
time: 6000,
});
isDragOver = false;
return;
}
if (files.length !== 3) {
@@ -148,7 +144,6 @@
text: "You must select or drop 3 files: history.csv, ratings.csv and watchlist.csv.",
time: 6000,
});
isDragOver = false;
return;
}
isLoading = true;
@@ -173,7 +168,6 @@
text: "Failed to read history, ratings or watchlist. Ensure you have attached 3 files: history.csv, ratings.csv and watchlist.csv.",
time: 6000,
});
isDragOver = false;
isLoading = false;
return;
}
@@ -254,7 +248,7 @@
data: JSON.stringify(toImport),
type: "movary",
};
goto("/import/process");
goto(resolve("/import/process"));
} catch (err) {
isLoading = false;
notify({ type: "error", text: "Failed to read files!" });
@@ -272,7 +266,6 @@
text: "File not found in dropped items. Please try again or refresh.",
time: 6000,
});
isDragOver = false;
return;
}
isLoading = true;
@@ -291,12 +284,12 @@
text: "Must be a Watcharr JSON export file",
});
isLoading = false;
isDragOver = false;
return;
}
// Build toImport array
const toImport: ImportedList[] = [];
const fileText = await readFile(new FileReader(), file);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const jsonData = JSON.parse(fileText) as any[];
let invalidStructureErrorOccurred = false;
let processedGame = false;
@@ -356,7 +349,7 @@
data: JSON.stringify(toImport),
type: "watcharr",
};
goto("/import/process");
goto(resolve("/import/process"));
} catch (err) {
isLoading = false;
notify({ type: "error", text: "Failed to read file!" });
@@ -374,7 +367,6 @@
text: "File not found in dropped items. Please try again or refresh.",
time: 6000,
});
isDragOver = false;
return;
}
isLoading = true;
@@ -393,7 +385,6 @@
text: "Your MyAnimeList export should be a xml file.",
});
isLoading = false;
isDragOver = false;
return;
}
const r = new FileReader();
@@ -405,7 +396,7 @@
data: r.result.toString(),
type: "myanimelist",
};
goto("/import/process");
goto(resolve("/import/process"));
}
},
false,
@@ -428,7 +419,6 @@
text: "File not found in dropped items. Please try again or refresh.",
time: 6000,
});
isDragOver = false;
return;
}
isLoading = true;
@@ -448,13 +438,13 @@
text: "Must be a Ryot JSON export file",
});
isLoading = false;
isDragOver = false;
return;
}
// Build toImport array
const toImport: ImportedList[] = [];
const fileText = await readFile(new FileReader(), file);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const jsonData = JSON.parse(fileText)["metadata"] as any[];
for (const v of jsonData) {
if (
@@ -522,13 +512,15 @@
datesWatched:
v.lot === "movie" && v.seen_history?.length
? v.seen_history.map((seen: any) => new Date(seen.ended_on))
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
v.seen_history.map((seen: any) => new Date(seen.ended_on))
: [],
// Episode ratings are on a separate field: "reviews"
watchedEpisodes:
v.lot === "show"
? v.seen_history?.map((episode: any) => ({
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
v.seen_history?.map((episode: any) => ({
status: episode.progress === "100" ? "FINISHED" : "WATCHING",
// Linear :( search the reviews for a match
@@ -537,6 +529,7 @@
Number(
(
v.reviews?.find(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(review: any) =>
review.show_season_number ===
episode.show_season_number &&
@@ -562,7 +555,7 @@
data: JSON.stringify(toImport),
type: "ryot",
};
goto("/import/process");
goto(resolve("/import/process"));
} catch (err) {
isLoading = false;
notify({ type: "error", text: "Failed to read file!" });
@@ -580,7 +573,6 @@
text: "File not found in dropped items. Please try again or refresh.",
time: 6000,
});
isDragOver = false;
return;
}
isLoading = true;
@@ -600,7 +592,6 @@
text: "Must be a TodoMovies backup file (.todomovieslist)",
});
isLoading = false;
isDragOver = false;
return;
}
@@ -614,7 +605,6 @@
text: "Failed to read export file. Ensure you have attached the correct file.",
time: 6000,
});
isDragOver = false;
isLoading = false;
return;
}
@@ -680,7 +670,7 @@
type: "todomovies",
};
goto("/import/process");
goto(resolve("/import/process"));
} catch (err) {
isLoading = false;
notify({ type: "error", text: "Failed to read files!" });
@@ -690,7 +680,7 @@
onMount(() => {
if (!localStorage.getItem("token")) {
goto("/login");
goto(resolve("/login"));
}
});
</script>
@@ -725,7 +715,7 @@
filesSelected={(f) => processFiles(f, "tmdb")}
/>
<button class="plain" onclick={() => goto("/import/trakt")}>
<button class="plain" onclick={() => goto(resolve("/import/trakt"))}>
<Icon i="trakt" wh="100%" />
<h4 class="norm">Trakt Import</h4>
</button>
+17 -10
View File
@@ -29,6 +29,7 @@
import { onDestroy } from "svelte";
import papa from "papaparse";
import Status from "@/lib/Status.svelte";
import { resolve } from "$app/paths";
interface ImportedListItemMultiProblem {
original: ImportedList;
@@ -66,7 +67,7 @@
const list = store.importedList;
if (!list) {
console.log("import/process, no list, returning to /import");
goto("/import");
goto(resolve("/import"));
return;
}
console.log("getList", list);
@@ -94,6 +95,7 @@
console.debug("parsed csv", s);
for (let i = 0; i < s.data.length; i++) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const el = s.data[i] as any;
if (el) {
// Skip if no name or tmdb id
@@ -135,6 +137,7 @@
// there are common keys between these types that we use below so should
// be okay with importing either.
importText = "IMDb";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const s = papa.parse<any>(list.data.trim(), { header: true });
console.debug("parsed csv", s);
let anySkipped = false;
@@ -152,6 +155,7 @@
});
for (let i = 0; i < s.data.length; i++) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const el = s.data[i] as any;
if (el) {
const imdbId = el["Const"];
@@ -349,6 +353,7 @@
data: "WATCHING",
customDate: new Date(startDateNode.textContent),
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any[];
}
if (
@@ -403,7 +408,6 @@
});
}
}
// TODO: remove duplicate names in list
}
function addRow(
@@ -461,14 +465,14 @@
) {
// Some items failed.. go to some-failed
store.parsedImportedList = rList;
goto("/import/some-failed");
goto(resolve("/import/some-failed"));
} else {
notify({
type: "success",
text: "All content successfully imported! Try refreshing if you are missing data.",
time: 15000,
});
goto("/");
goto(resolve("/"));
}
}
@@ -608,6 +612,8 @@
</tr>
</thead>
<tbody>
<!-- TODO: Fix this to use a keyed each somehow (need unique id for key) -->
<!-- eslint-disable-next-line svelte/require-each-key -->
{#each rList as l}
<tr>
{#if isImporting}
@@ -627,13 +633,13 @@
</div>
</td>
{/if}
<td
><input
<td>
<input
class="plain"
bind:value={l.name}
disabled={isImporting}
/></td
>
/>
</td>
<td class="year">
<input
class="plain"
@@ -706,7 +712,8 @@
</tbody>
</table>
<div class="btns">
<button onclick={() => goto("/import")}><Icon i="arrow" />Back</button
<button onclick={() => goto(resolve("/import"))}
><Icon i="arrow" />Back</button
>
<button onclick={() => changeAllStatuses()} disabled={isImporting}>
Change Statuses
@@ -747,7 +754,7 @@
}}
>
<PosterList type="vertical">
{#each importMultiItem.results as r}
{#each importMultiItem.results as r (r.ids)}
<Poster
media={r}
small={true}
@@ -12,6 +12,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { store } from "@/store.svelte";
import { ImportResponseType, type ImportedList } from "@/types";
import { onMount } from "svelte";
@@ -36,7 +37,7 @@
}
console.log("failedlen", failed.length);
} else {
goto("/import");
goto(resolve("/import"));
}
});
</script>
@@ -51,6 +52,8 @@
{#if failed}
<ul>
<!-- TODO: Fix this to use a keyed each somehow (need unique id for key) -->
<!-- eslint-disable-next-line svelte/require-each-key -->
{#each failed as l}
<li>
<span>{l.name}</span>
@@ -36,7 +36,7 @@
let nextLoadParams: {
page: number;
[x: string]: any;
[x: string]: unknown;
} = $derived({
page: dataLoader.state.page + 1,
...store.sortAndFiltersForQueryParams,
+1 -1
View File
@@ -42,7 +42,7 @@
</tr>
</thead>
<tbody>
{#each allUsers as u}
{#each allUsers as u (u.id)}
{@const joinDate = new Date(u.createdAt)}
<tr>
<td class="username">
@@ -23,7 +23,6 @@
let { user = $bindable(), onClose }: Props = $props();
let error: string | undefined = $state();
let formDisabled = false;
// Things we have changed
let changedPerms = false;
@@ -51,7 +50,7 @@
text: "Changes saved!",
});
onClose();
} catch (err: any) {
} catch (err) {
console.error("Failed to save user!", err);
error = ReqerError.getMsg(err, "Failed to save");
}
@@ -65,7 +64,7 @@
</script>
<Modal
title={`Edit User`}
title="Edit User"
desc={`Configuring ${user.username}`}
maxWidth="500px"
{onClose}
+11 -15
View File
@@ -30,6 +30,7 @@
import WatchedDeleteBtn from "@/lib/content/WatchedDeleteBtn.svelte";
import TopCrewList from "@/lib/content/TopCrewList.svelte";
import { activityRemovedHook } from "@/lib/activity.js";
import Genres from "@/lib/content/Genres.svelte";
let { data } = $props();
@@ -37,7 +38,7 @@
let jellyfinUrl: string | undefined = $state();
let arrRequestButtonComp: ArrRequestButton | undefined = $state();
let movie: Media | undefined = $state();
let pageError: Error | undefined = $state();
let pageError: unknown | undefined = $state();
$effect(() => {
(async () => {
@@ -64,7 +65,7 @@
} else {
movie = undefined;
}
} catch (err: any) {
} catch (err) {
movie = undefined;
pageError = err;
}
@@ -148,17 +149,7 @@
<span class="quick-info">
<span>{movie.runtime} min</span>
{#if movie.genres && movie.genres?.length > 0}
<div>
{#each movie.genres as g, i}
<span>
{g.name}{i !== movie.genres.length - 1 ? ", " : ""}
</span>
{/each}
</div>
{:else}
<span>Unknown Genres</span>
{/if}
<Genres genres={movie.genres} />
</span>
<ExpandableText text={movie.summary} style="margin-bottom: 18px;" />
@@ -166,7 +157,12 @@
<div class="btns">
<ViewTrailerButton videos={movie.videos} />
{#if jellyfinUrl}
<a class="btn" href={jellyfinUrl} target="_blank">
<a
class="btn"
href={jellyfinUrl}
rel="external"
target="_blank"
>
{#if localStorage.getItem("useEmby")}
<Icon i="emby" wh={14} />Play On Emby
{:else}
@@ -263,7 +259,7 @@
{#if credits.cast?.length > 0}
<HorizontalList title="Cast">
{#each credits.cast?.slice(0, 50) as cast}
{#each credits.cast?.slice(0, 50) as cast (cast.id)}
<PersonPoster
id={cast.id}
name={cast.name}
+5 -3
View File
@@ -19,7 +19,7 @@
let { data } = $props();
let person: PersonDetailsResponse | undefined = $state();
let pageError: Error | undefined = $state();
let pageError: unknown | undefined = $state();
let sortOption = $state("Vote count");
let credits: PersonCreditsResponse | undefined = $state();
let onMyListFilter = $state(false);
@@ -46,7 +46,7 @@
person = await getPerson(data.personId);
await updatePersonCredits();
sortCredits(sortOption);
} catch (err: any) {
} catch (err) {
person = undefined;
pageError = err;
}
@@ -138,7 +138,9 @@
<div class="details">
<span class="title-container">
<a href={person.homepage} target="_blank">{person.name}</a>
<a href={person.homepage} rel="external" target="_blank">
{person.name}
</a>
<span></span>
</span>
+3 -2
View File
@@ -19,6 +19,7 @@
import { toggleTheme } from "@/lib/util/theme";
import ExportListModal from "./modals/ExportListModal.svelte";
import { ReqerError } from "@/lib/util/fetch";
import { resolve } from "$app/paths";
let user = $derived(store.userInfo);
let settings = $derived(store.userSettings);
@@ -239,7 +240,7 @@
>
<Checkbox
name="privateThoughts"
disabled={privateDisabled}
disabled={privateThoughtsDisabled}
value={settings?.privateThoughts}
toggled={(on) => {
privateThoughtsDisabled = true;
@@ -309,7 +310,7 @@
<RatingSetting />
<div class="row btns">
<button onclick={() => goto("/import")}>Import</button>
<button onclick={() => goto(resolve("/import"))}>Import</button>
<button onclick={() => (exportModalOpen = true)}>Export</button>
{#if user?.type !== UserType.Plex && user?.type !== UserType.Jellyfin}
<button
@@ -13,7 +13,7 @@
const nid = notify({ text: "Exporting", type: "loading" });
try {
// We re-fetch, to ensure data we export is up to date.
const r = await req.get<any>("/watched");
const r = await req.get<unknown[]>("/watched");
console.log(r);
if (!r || r?.length <= 0) {
notify({
@@ -1,6 +1,4 @@
<script lang="ts">
import { preventDefault } from "svelte/legacy";
import Modal from "@/lib/Modal.svelte";
import type { ChangePasswordForm } from "@/types";
import { changeUserPassword } from "$lib/util/api";
@@ -21,7 +19,7 @@
}),
}: Props = $props();
let error: string = $state();
let error: string | undefined = $state();
let errs: string[] = [];
function checkForm() {
@@ -67,6 +65,7 @@
}
function handleSubmit(ev: SubmitEvent) {
ev.preventDefault();
checkForm();
if (!error) {
console.log(
@@ -101,7 +100,7 @@
{#if error}
<span class="error">{error}!</span>
{/if}
<form onsubmit={preventDefault(handleSubmit)}>
<form onsubmit={handleSubmit}>
<div class="form-input-container">
<div class="form-input">
<!--Hiding username info as it is still useful to password managers-->
@@ -156,7 +156,7 @@
</h4>
<span>Syncing has finished, but with errors:</span>
<ul>
{#each latestJobStatus?.errors as e}
{#each latestJobStatus?.errors as e (e)}
<li>{e}</li>
{/each}
</ul>
+2 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { resolve } from "$app/paths";
import { req } from "@/lib/util/api";
import { notify } from "@/lib/util/notify";
import { store } from "@/store.svelte";
@@ -32,7 +33,7 @@
if (store.userInfo) {
store.userInfo.permissions = UserPermission.PERM_ADMIN;
}
goto("/");
goto(resolve("/"));
})
.catch((err) => {
console.error("Failed to use admin token", err);
+3 -2
View File
@@ -25,6 +25,7 @@
} from "@/lib/util/paginatedLoader.svelte.js";
import PageTitle from "@/lib/generic/PageTitle.svelte";
import MediaTypeFilter from "@/lib/search/MediaTypeFilter.svelte";
import { resolve } from "$app/paths";
let { data } = $props();
@@ -95,7 +96,7 @@
}
// Running the goto will cause afterNavigate hook to be called,
// which will run a fresh search, so nothing else to do here.
goto(`?${curLocation.searchParams.toString()}`);
goto(resolve(`/search?${curLocation.searchParams.toString()}`));
}
async function searchUsers(query: string) {
@@ -148,7 +149,7 @@
curLocation.searchParams.delete("preferMyList");
// Running the goto will cause afterNavigate hook to be called,
// which will run a fresh search, so nothing else to do here.
goto(`?${curLocation.searchParams.toString()}`);
goto(resolve(`/search?${curLocation.searchParams.toString()}`));
}
</script>
+25 -14
View File
@@ -21,6 +21,7 @@
import RegionDropDown from "@/lib/RegionDropDown.svelte";
import TaskScheduleModal from "./modals/TaskScheduleModal.svelte";
import TrustedHeaderAuthModal from "./modals/TrustedHeaderAuthModal.svelte";
import { resolve } from "$app/paths";
let serverConfig: ServerConfig | undefined = $state();
let jellyfinOrEmby = $derived(serverConfig?.USE_EMBY ? "Emby" : "Jellyfin");
@@ -49,7 +50,7 @@
export function updateServerConfig<K extends keyof ServerConfig>(
name: K,
value: ServerConfig[K],
done?: (respData?: any) => void,
done?: (respData?: object) => void,
) {
if (!serverConfig) {
console.error("updateServerConfig: No server config to update!");
@@ -64,7 +65,7 @@
ep = "/server/config/plex_host";
}
req
.postWhole<any>(ep, { key: name, value: value })
.postWhole<object>(ep, { key: name, value: value })
.then((r) => {
if (r.status === 200) {
serverConfig![name] = value;
@@ -105,7 +106,12 @@
{#await getServerStats()}
<Spinner />
{:then stats}
<Stat name="Users" value={stats.users} href="/manage_users" large />
<Stat
name="Users"
value={stats.users}
href={resolve("/manage_users")}
large
/>
<Stat name="Private Users" value={stats.privateUsers} large />
<Stat name="Watched Movies" value={stats.watchedMovies} large />
<Stat name="Watched Shows" value={stats.watchedShows} large />
@@ -115,14 +121,14 @@
<Stat
name="Most Watched Movie"
value={stats.mostWatchedMovie.title}
href="/movie/{stats.mostWatchedMovie.tmdbId}"
href={resolve("/movie/{stats.mostWatchedMovie.tmdbId}")}
/>
{/if}
{#if stats.mostWatchedShow?.title}
<Stat
name="Most Watched Show"
value={stats.mostWatchedShow.title}
href="/tv/{stats.mostWatchedShow.tmdbId}"
href={resolve("/tv/{stats.mostWatchedShow.tmdbId}")}
/>
{/if}
{:catch err}
@@ -205,7 +211,12 @@
serverConfig!.PLEX_HOST,
(rData) => {
plexHostDisabled = false;
serverConfig!.PLEX_MACHINE_ID = rData?.PLEX_MACHINE_ID;
serverConfig!.PLEX_MACHINE_ID =
rData &&
"PLEX_MACHINE_ID" in rData &&
rData?.PLEX_MACHINE_ID
? String(rData?.PLEX_MACHINE_ID)
: undefined;
},
);
}}
@@ -265,7 +276,7 @@
<SettingButton
title="Task Schedule"
desc="View and configure server task schedule."
icon={"arrow"}
icon="arrow"
onClick={() => {
taskScheduleModalOpen = true;
}}
@@ -279,7 +290,7 @@
<SettingButton
title="Trusted Header Authentication"
desc="Configure trusted header single sign-on."
icon={"arrow"}
icon="arrow"
onClick={() => {
headerSSOModalOpen = true;
}}
@@ -319,8 +330,8 @@
</Setting>
<Setting title="Sonarr">
{#if serverConfig.SONARR?.length > 0}
{#each serverConfig.SONARR as server}
{#if serverConfig.SONARR && serverConfig.SONARR?.length > 0}
{#each serverConfig.SONARR as server (server.name)}
<SettingButton
title={server.name}
desc={`Configure server at ${server.host}`}
@@ -338,7 +349,7 @@
icon="add"
onClick={() => {
let name = "Sonarr";
if (serverConfig!.SONARR?.length > 0) {
if (serverConfig?.SONARR && serverConfig.SONARR.length > 0) {
// if this still exists ya on yur own
name = `Sonarr${serverConfig!.SONARR.length + 1}`;
}
@@ -350,8 +361,8 @@
</Setting>
<Setting title="Radarr">
{#if serverConfig.RADARR?.length > 0}
{#each serverConfig.RADARR as server}
{#if serverConfig.RADARR && serverConfig.RADARR?.length > 0}
{#each serverConfig.RADARR as server (server.name)}
<SettingButton
title={server.name}
desc={`Configure server at ${server.host}`}
@@ -369,7 +380,7 @@
icon="add"
onClick={() => {
let name = "Radarr";
if (serverConfig!.RADARR?.length > 0) {
if (serverConfig?.RADARR && serverConfig.RADARR.length > 0) {
// if this still exists ya on yur own
name = `Radarr${serverConfig!.RADARR.length + 1}`;
}
@@ -98,7 +98,7 @@
text: isEditing ? "Changes saved!" : "Server added successfully!",
});
onClose();
} catch (err: any) {
} catch (err) {
console.error("Failed to save server!", err);
error = ReqerError.getMsg(
err,
@@ -116,7 +116,7 @@
text: "Removed server",
});
onClose();
} catch (err: any) {
} catch (err) {
console.error("Failed to remove server!", err);
error = ReqerError.getMsg(err, "Failed to remove");
}
@@ -106,7 +106,7 @@
text: isEditing ? "Changes saved!" : "Server added successfully!",
});
onClose();
} catch (err: any) {
} catch (err) {
console.error("Failed to save server!", err);
error = ReqerError.getMsg(
err,
@@ -124,7 +124,7 @@
text: "Removed server",
});
onClose();
} catch (err: any) {
} catch (err) {
console.error("Failed to remove server!", err);
error = ReqerError.getMsg(err, "Failed to remove");
}
@@ -98,7 +98,7 @@
{#if taskSchedule?.length <= 0}
<Spinner />
{:else}
{#each taskSchedule as task}
{#each taskSchedule as task (task.name)}
{@const nextRun = toRelativeTime(
(new Date(task.nextRun).getTime() - now) / 1000,
)}
@@ -43,7 +43,7 @@
text: "Changes saved!",
});
onClose();
} catch (err: any) {
} catch (err) {
console.error("Failed to save twitch cfg!", err);
error = ReqerError.getMsg(err, "Failed to save");
}
@@ -52,7 +52,7 @@
</script>
<Modal
title={"Twitch Config"}
title="Twitch Config"
desc="Setup your twitch application to enable game support."
{onClose}
>
+1 -1
View File
@@ -30,7 +30,7 @@
let nextLoadParams: {
page: number;
[x: string]: any;
[x: string]: unknown;
} = $derived({
page: dataLoader.state.page + 1,
...store.sortAndFiltersForQueryParams,
+11 -15
View File
@@ -38,6 +38,7 @@
import { activityRemovedHook } from "@/lib/activity.js";
import CountAsPlayModal from "@/lib/watched/CountAsPlayModal.svelte";
import { createSignal, type Signal } from "@/lib/util/signal.js";
import Genres from "@/lib/content/Genres.svelte";
let { data } = $props();
@@ -45,7 +46,7 @@
let jellyfinUrl: string | undefined = $state();
let arrRequestButtonComp: ArrRequestButton | undefined = $state();
let show: Media | undefined = $state();
let pageError: Error | undefined = $state();
let pageError: unknown | undefined = $state();
let countAsPlayModalSignal: Signal<boolean> | undefined = $state();
$effect(() => {
@@ -73,7 +74,7 @@
} else {
show = undefined;
}
} catch (err: any) {
} catch (err) {
show = undefined;
pageError = err;
}
@@ -170,17 +171,7 @@
/>
<span class="quick-info">
{#if show.genres && show.genres?.length > 0}
<div>
{#each show.genres as g, i}
<span
>{g.name}{i !== show.genres.length - 1 ? ", " : ""}</span
>
{/each}
</div>
{:else}
<span>Unknown Genres</span>
{/if}
<Genres genres={show.genres} />
</span>
<ExpandableText text={show.summary} style="margin-bottom: 18px;" />
@@ -188,7 +179,12 @@
<div class="btns">
<ViewTrailerButton videos={show.videos} />
{#if jellyfinUrl}
<a class="btn" href={jellyfinUrl} target="_blank">
<a
class="btn"
href={jellyfinUrl}
rel="external"
target="_blank"
>
{#if localStorage.getItem("useEmby")}
<Icon i="emby" wh={14} />Play On Emby
{:else}
@@ -288,7 +284,7 @@
{#if credits.cast?.length > 0}
<HorizontalList title="Cast">
{#each credits.cast?.slice(0, 50) as cast}
{#each credits.cast?.slice(0, 50) as cast (cast.id)}
<PersonPoster
id={cast.id}
name={cast.name}
+1 -1
View File
@@ -1,5 +1,5 @@
import { error } from "@sveltejs/kit";
import type { PageLoad } from "../../search/[query]/$types";
import type { PageLoad } from "./$types";
export const load = (async ({ params }) => {
const { id } = params;
+13 -12
View File
@@ -2,11 +2,12 @@
import { goto } from "$app/navigation";
import { page } from "$app/state";
import Icon from "@/lib/Icon.svelte";
import { type AvailableAuthProviders } from "@/types";
import { type AuthResponse, type AvailableAuthProviders } from "@/types";
import { noAuthReq } from "@/lib/util/api";
import { onMount } from "svelte";
import { notify, unNotify } from "@/lib/util/notify";
import { ReqerError } from "@/lib/util/fetch";
import { resolve } from "$app/paths";
let error: string | undefined = $state();
let login = $state(true);
@@ -19,7 +20,7 @@
onMount(() => {
if (localStorage.getItem("token")) {
goto("/");
goto(resolve("/"));
}
if (!error && page.url.searchParams.get("again")) {
@@ -57,7 +58,7 @@
console.log(
"AvailableAuth: Server is in setup.. navigating to web setup page.",
);
goto("/setup");
goto(resolve("/setup"));
}
availableProviders = r.available;
apHeader = availableProviders?.includes("header");
@@ -90,11 +91,11 @@
const nid = notify({ text: "Logging in", type: "loading" });
noAuthReq
.post(`/auth${login ? `/${customAuthEP}` : "/register"}`, {
.post<AuthResponse>(`/auth${login ? `/${customAuthEP}` : "/register"}`, {
username: user,
password: pass,
})
.then((resp: any) => {
.then((resp) => {
if (resp?.token) {
console.log("Received token... logging in.");
localStorage.setItem("token", resp.token);
@@ -103,7 +104,7 @@
} else {
localStorage.removeItem("useEmby");
}
goto("/");
goto(resolve("/"));
notify({ id: nid, text: `Welcome ${user}!`, type: "success" });
}
})
@@ -127,15 +128,15 @@
}
const nid = notify({ text: "Logging in", type: "loading" });
noAuthReq
.post("/auth/plex", {
.post<AuthResponse>("/auth/plex", {
token,
clientIdentifier: p.clientId,
})
.then((resp: any) => {
.then((resp) => {
if (resp?.token) {
console.log("Received token... logging in.");
localStorage.setItem("token", resp.token);
goto("/");
goto(resolve("/"));
notify({ id: nid, text: `Welcome!`, type: "success" });
}
})
@@ -154,12 +155,12 @@
function proxyLogin(auto = false) {
const nid = notify({ text: "Logging in", type: "loading" });
noAuthReq
.post(`/auth/proxy`)
.then((resp: any) => {
.post<AuthResponse>(`/auth/proxy`)
.then((resp) => {
if (resp?.token) {
console.log("Received token... logging in.");
localStorage.setItem("token", resp.token);
goto("/");
goto(resolve("/"));
notify({ id: nid, text: `Welcome!`, type: "success" });
}
})
+6 -5
View File
@@ -1,23 +1,24 @@
<script lang="ts">
import { goto } from "$app/navigation";
import type { AvailableAuthProviders } from "@/types";
import type { AuthResponse, AvailableAuthProviders } from "@/types";
import { onMount } from "svelte";
import { notify, unNotify } from "@/lib/util/notify";
import { noAuthReq } from "@/lib/util/api";
import { ReqerError } from "@/lib/util/fetch";
import { resolve } from "$app/paths";
let error: string | undefined = $state();
onMount(() => {
if (localStorage.getItem("token")) {
goto("/");
goto(resolve("/"));
}
noAuthReq.get<AvailableAuthProviders>("/auth/available").then((r) => {
if (r) {
if (!r.isInSetup) {
console.log("Server not in setup.. navigating to login page.");
goto("/login");
goto(resolve("/(plain)/login"));
}
}
});
@@ -36,7 +37,7 @@
const nid = notify({ text: "Setting Up Admin User", type: "loading" });
noAuthReq
.post<any>("/setup/create_admin", {
.post<AuthResponse>("/setup/create_admin", {
username: user,
password: pass,
})
@@ -44,7 +45,7 @@
if (resp.token) {
console.log("Received token... logging in.");
localStorage.setItem("token", resp.token);
goto("/");
goto(resolve("/"));
notify({ id: nid, text: `Welcome ${user}!`, type: "success" });
}
})
+2 -1
View File
@@ -1,6 +1,7 @@
<script>
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { resolve } from "$app/paths";
</script>
<div>
@@ -16,7 +17,7 @@
<h4 class="norm">We couldn't load this page</h4>
{/if}
<div class="btns">
<button onclick={() => goto("/")}>Home</button>
<button onclick={() => goto(resolve("/"))}>Home</button>
<button onclick={() => location.reload()}>Refresh</button>
</div>
</div>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import Notifications from "@/lib/notifications.svelte";
import Notifications from "@/lib/Notifications.svelte";
import { onMount } from "svelte";
import { pwaInfo } from "virtual:pwa-info";
+7 -5
View File
@@ -13,15 +13,17 @@ import type { Notification } from "./lib/util/notify";
import { browser } from "$app/environment";
import { toggleTheme } from "./lib/util/theme";
export const defaultSort = ["DATEADDED", "DOWN"];
type ActiveSort = [string, string | undefined] | [];
export const defaultSort: ActiveSort = ["DATEADDED", "DOWN"];
interface Store {
userInfo: PrivateUser | undefined;
userSettings: UserSettings | undefined;
notifications: Notification[];
activeSort: string[];
activeSort: ActiveSort;
activeFilters: Filters;
sortAndFiltersForQueryParams: {};
sortAndFiltersForQueryParams: object;
appTheme: Theme;
importedList:
| {
@@ -67,7 +69,7 @@ const _store: Store = $state({
const updateSortAndFiltersForQueryParams = () => {
try {
const qp: any = {};
const qp: Record<string, string> = {};
if (store.activeSort?.length === 2) {
qp.sort = store.activeSort[0];
qp.sortDir = store.activeSort[1] === "UP" ? "asc" : "desc";
@@ -280,7 +282,7 @@ function rehydrateStore() {
$state.snapshot(store.appTheme),
);
} else {
let defTheme: Theme = "system";
const defTheme: Theme = "system";
_store.appTheme = defTheme;
toggleTheme(defTheme, false);
console.debug(
+28 -9
View File
@@ -75,6 +75,10 @@ export enum UserType {
Proxy = 3,
}
export interface AuthResponse {
token: string;
}
interface dbModel {
id: number;
createdAt: string;
@@ -174,6 +178,21 @@ export interface WatchedUpdateResponse {
newActivity: Activity;
}
export type WatchedSort =
"DATEADDED" | "LASTCHANGED" | "LASTFIN" | "RATING" | "ALPHA" | "DATERELEASED";
export type SortDirection = "asc" | "desc";
export interface WatchedGetPageRequest {
// Sorting type.
sort?: WatchedSort;
// Sorting direction (asc or desc).
sortDir?: SortDirection;
// Filtering options.
type?: SupportedMedia[];
status?: WatchedStatus[];
}
export interface ActivityUpdateRequest {
customDate: string;
}
@@ -567,15 +586,15 @@ export interface ManagedUser {
}
export interface ServerConfig {
DEFAULT_COUNTRY: string;
JELLYFIN_HOST: string;
DEFAULT_COUNTRY?: string;
JELLYFIN_HOST?: string;
USE_EMBY: boolean;
SIGNUP_ENABLED: boolean;
TMDB_KEY: string;
PLEX_HOST: string;
PLEX_MACHINE_ID: string;
SONARR: SonarrSettings[];
RADARR: RadarrSettings[];
TMDB_KEY?: string;
PLEX_HOST?: string;
PLEX_MACHINE_ID?: string;
SONARR?: SonarrSettings[];
RADARR?: RadarrSettings[];
TWITCH?: TwitchSettings;
DEBUG: boolean;
}
@@ -656,7 +675,7 @@ export interface QualityProfile {
source: string;
resolution: number;
};
items: any[];
items: unknown[];
allowed: boolean;
name?: string;
id?: number;
@@ -668,7 +687,7 @@ export interface RootFolder {
path: string;
accessible: boolean;
freeSpace: number;
unmappedFolders: any[];
unmappedFolders: unknown[];
id: number;
}
+1 -16
View File
@@ -10,22 +10,7 @@
"sourceMap": true,
"strict": true,
"types": ["vite-plugin-pwa/client", "vite-plugin-pwa/info"]
},
// Includes merged from ./svelte-kit/tsconfig.json, since we want to add .env.d.ts,
// and typescript wont merge both the include arrays.. only override.
"include": [
"./.svelte-kit/ambient.d.ts",
"./.svelte-kit/types/**/$types.d.ts",
"./vite.config.js",
"./vite.config.ts",
"./src/**/*.js",
"./src/**/*.ts",
"./src/**/*.svelte",
"./tests/**/*.js",
"./tests/**/*.ts",
"./tests/**/*.svelte",
"./env.d.ts"
]
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes