mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
fix all linting issues and upgrade the rest of devDependencies
now i should probably test the whole app waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaauuuuuuuuuugugghghghghghghghghg
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Generated
+2872
-2588
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -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": {
|
||||
|
||||
Vendored
@@ -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
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(
|
||||
" ",
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
let { embed, closed }: Props = $props();
|
||||
|
||||
let modalDiv: HTMLDivElement = $state();
|
||||
let modalDiv: HTMLDivElement | undefined = $state();
|
||||
|
||||
onMount(() => {
|
||||
// For better experience on keyboard.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
let { type = "wrapped", children }: Props = $props();
|
||||
|
||||
let ulEl: HTMLUListElement = $state();
|
||||
let ulEl: HTMLUListElement | undefined = $state();
|
||||
|
||||
onMount(() => {
|
||||
if (ulEl) {
|
||||
|
||||
@@ -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' : ''}"
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
pos: "bot",
|
||||
}}
|
||||
>
|
||||
<Icon i={"tag"} wh={19} />
|
||||
<Icon i="tag" wh={19} />
|
||||
</button>
|
||||
|
||||
{#if menuOpen}
|
||||
|
||||
+3
-17
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
let nextLoadParams: {
|
||||
page: number;
|
||||
[x: string]: any;
|
||||
[x: string]: unknown;
|
||||
} = $derived({
|
||||
page: dataLoader.state.page + 1,
|
||||
...store.sortAndFiltersForQueryParams,
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user