Remove sveltekit dependency, create simple test project
@@ -1,6 +1,7 @@
|
||||
node_modules
|
||||
.svelte-kit
|
||||
build
|
||||
dist
|
||||
.idea
|
||||
.env
|
||||
.DS_Store
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
PUBLIC_RADARR_API_KEY=yourapikeyhere
|
||||
PUBLIC_RADARR_BASE_URL=http://127.0.0.1:7878
|
||||
|
||||
PUBLIC_SONARR_API_KEY=yourapikeyhere
|
||||
PUBLIC_SONARR_BASE_URL=http://127.0.0.1:8989
|
||||
|
||||
PUBLIC_JELLYFIN_API_KEY=yourapikeyhere
|
||||
PUBLIC_JELLYFIN_BASE_URL=http://127.0.0.1:8096
|
||||
@@ -6,8 +6,32 @@ node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.vercel
|
||||
.output
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
/config/*.sqlite
|
||||
/dist
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
23
.gitignore
vendored
@@ -12,3 +12,26 @@ vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
/config/*.sqlite
|
||||
/dist
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/dist
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"appId": "me.aleksilassila.reiverr",
|
||||
"productName": "Reiverr",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"mac": {
|
||||
"asar": false
|
||||
},
|
||||
"win": {
|
||||
"asar": false,
|
||||
"target": "msi"
|
||||
},
|
||||
"asar": false,
|
||||
"files": [
|
||||
"src/electron.cjs",
|
||||
{
|
||||
"from": "build",
|
||||
"to": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
index.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="./public/favicon.png" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||
<title>Vite + Svelte + TS</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Nunito+Sans:wght@200;300;400;500;600;700;800;900;1000&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Montserrat:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body class="bg-stone-950 min-h-screen text-white touch-manipulation relative -z-10">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
7664
package-lock.json
generated
81
package.json
@@ -1,67 +1,58 @@
|
||||
{
|
||||
"name": "reiverr",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aleksilassila/reiverr"
|
||||
},
|
||||
"main": "src/electron.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev --host",
|
||||
"dev": "vite --open",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"deploy": "PORT=9494 NODE_ENV=production node build/",
|
||||
"deploy:electron": "vite build && electron-builder -mw --x64 --config build.config.json; electron-builder -m --arm64 --config build.config.json",
|
||||
"test": "playwright test",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"test:unit": "vitest",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.10",
|
||||
"@neoconfetti/svelte": "^1.0.0",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/adapter-static": "2.0.3",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/node": "^20.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"classnames": "^2.3.2",
|
||||
"electron": "^26.1.0",
|
||||
"electron-builder": "^24.6.3",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.26.0",
|
||||
"openapi-typescript": "^6.2.7",
|
||||
"postcss": "^8.4.24",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"svelte": "^4.1.0",
|
||||
"svelte-check": "^3.0.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.1.3",
|
||||
"vite": "^4.3.0",
|
||||
"vitest": "^0.25.3"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@jellyfin/sdk": "^0.7.0",
|
||||
"axios": "^1.4.0",
|
||||
"hls.js": "^1.4.6",
|
||||
"openapi-fetch": "^0.2.1",
|
||||
"radix-icons-svelte": "^1.2.1",
|
||||
"vitest": "^0.25.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sqlite3": "^5.1.6",
|
||||
"svelte-i18n": "^3.7.0",
|
||||
"@jellyfin/sdk": "^0.8.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
||||
"@tsconfig/svelte": "^5.0.2",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"axios": "^1.6.2",
|
||||
"classnames": "^2.4.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"hls.js": "^1.4.14",
|
||||
"openapi-fetch": "^0.8.2",
|
||||
"openapi-typescript": "^6.7.3",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"radix-icons-svelte": "^1.2.1",
|
||||
"svelte": "^3.59.1",
|
||||
"svelte-check": "^3.6.2",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"svelte-navigator": "^3.2.2",
|
||||
"tailwind-scrollbar-hide": "^1.1.7",
|
||||
"typeorm": "^0.3.17"
|
||||
"tailwindcss": "^3.4.0",
|
||||
"terser": "^5.26.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.1",
|
||||
"vite-plugin-singlefile": "^0.13.5"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 846 B After Width: | Height: | Size: 846 B |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 684 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
56
src/App.svelte
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import Carousel from "./lib/components/Carousel/Carousel.svelte";
|
||||
import I18n from "./lib/components/Lang/I18n.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import CarouselPlaceholderItems from "./lib/components/Carousel/CarouselPlaceholderItems.svelte";
|
||||
import { Link, navigate, Route, Router } from "svelte-navigator";
|
||||
import { fade } from "svelte/transition";
|
||||
import { networks } from "./lib/discover";
|
||||
import NetworkCard from "./lib/components/NetworkCard.svelte";
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === "ArrowRight") {
|
||||
console.log("right");
|
||||
navigate("/about");
|
||||
}
|
||||
if (event.key === "ArrowLeft") {
|
||||
console.log("left");
|
||||
navigate("/");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<I18n />
|
||||
<main class="bg-stone-950 text-white">
|
||||
<Router>
|
||||
<nav>
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="about">About</Link>
|
||||
</nav>
|
||||
|
||||
<Carousel>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_("discover.upcomingSeries")}
|
||||
</div>
|
||||
<CarouselPlaceholderItems />
|
||||
</Carousel>
|
||||
|
||||
<Carousel>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_("discover.TVNetworks")}
|
||||
</div>
|
||||
{#each Object.values(networks) as network (network.tmdbNetworkId)}
|
||||
<NetworkCard {network} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
|
||||
<Route path="/">
|
||||
<div transition:fade|global>Home path</div>
|
||||
</Route>
|
||||
<Route path="about">
|
||||
<div transition:fade|global>about path</div>
|
||||
</Route>
|
||||
</Router>
|
||||
</main>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
31
src/app.html
@@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="background-color: black">
|
||||
<head>
|
||||
<title>Reiverr</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Nunito+Sans:wght@200;300;400;500;600;700;800;900;1000&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Montserrat:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body
|
||||
data-sveltekit-preload-data="hover"
|
||||
class="bg-stone-950 min-h-screen text-white touch-manipulation relative -z-10"
|
||||
>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,51 +0,0 @@
|
||||
process.env.PORT = '9494';
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
(async () => {
|
||||
await import('../build/index.js');
|
||||
// const serveURL = serve({ directory: '.' });
|
||||
const port = process.env.PORT || 5173;
|
||||
let mainWindow;
|
||||
|
||||
const createWindow = () => {
|
||||
if (!mainWindow) {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mainWindow.once('close', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
loadSite(port);
|
||||
|
||||
return mainWindow;
|
||||
};
|
||||
|
||||
function loadSite(port) {
|
||||
mainWindow.loadURL(`http://localhost:${port}`).catch((e) => {
|
||||
console.log('Error loading URL, retrying', e);
|
||||
setTimeout(() => {
|
||||
loadSite(port);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
app.on('activate', () => {
|
||||
if (!mainWindow) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
})();
|
||||
@@ -1,4 +0,0 @@
|
||||
import TypeOrm from '$lib/db';
|
||||
import 'reflect-metadata';
|
||||
|
||||
await TypeOrm.getDb();
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { settings } from '$lib/stores/settings.store';
|
||||
import { addMessages, init, locale } from 'svelte-i18n';
|
||||
|
||||
import de from '../../lang/de.json';
|
||||
@@ -7,6 +6,7 @@
|
||||
import es from '../../lang/es.json';
|
||||
import fr from '../../lang/fr.json';
|
||||
import it from '../../lang/it.json';
|
||||
import { settings } from '../../stores/settings.store';
|
||||
|
||||
addMessages('de', de);
|
||||
addMessages('en', en);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { TitleId } from '$lib/types';
|
||||
import { fly } from 'svelte/transition';
|
||||
import MoviePage from '../../../routes/movie/[id]/MoviePage.svelte';
|
||||
import SeriesPage from '../../../routes/series/[id]/SeriesPage.svelte';
|
||||
import MoviePage from '../MoviePage.svelte';
|
||||
import SeriesPage from '../SeriesPage.svelte';
|
||||
import { modalStack } from '../../stores/modal.store';
|
||||
import PersonPage from '../../../routes/person/[id]/PersonPage.svelte';
|
||||
import PersonPage from '../PersonPage.svelte';
|
||||
|
||||
export let titleId: TitleId;
|
||||
export let modalId: symbol;
|
||||
|
||||
8
src/main.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app'),
|
||||
})
|
||||
|
||||
export default app
|
||||
@@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import I18n from '$lib/components/Lang/I18n.svelte';
|
||||
import DynamicModal from '$lib/components/Modal/DynamicModal.svelte';
|
||||
import Navbar from '$lib/components/Navbar/Navbar.svelte';
|
||||
import UpdateChecker from '$lib/components/UpdateChecker.svelte';
|
||||
import { type SettingsValues, defaultSettings, settings } from '$lib/stores/settings.store';
|
||||
import { writable } from 'svelte/store';
|
||||
import '../app.css';
|
||||
import Notifications from '$lib/components/Notification/Notifications.svelte';
|
||||
|
||||
// export let data: LayoutServerData;
|
||||
// settings.set(data.settings);
|
||||
</script>
|
||||
|
||||
<!-- {#if data.isApplicationSetUp} -->
|
||||
<I18n />
|
||||
<div class="app">
|
||||
<Navbar />
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
{#key $page.url.pathname}
|
||||
<DynamicModal />
|
||||
{/key}
|
||||
<Notifications />
|
||||
<UpdateChecker />
|
||||
</div>
|
||||
<!-- {:else} -->
|
||||
<!-- <SetupRequired missingEnvironmentVariables={data.missingEnvironmentVariables} /> -->
|
||||
<!-- {/if} -->
|
||||
@@ -1,7 +0,0 @@
|
||||
// import { dev } from '$app/environment';
|
||||
|
||||
// Disable SSR when running the dev server
|
||||
// This is a fix to vite dev server freezing on mac :(
|
||||
// https://github.com/vitejs/vite/issues/11468
|
||||
// export const ssr = !dev;
|
||||
export const ssr = false;
|
||||
@@ -1,310 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
getJellyfinBackdrop,
|
||||
getJellyfinContinueWatching,
|
||||
getJellyfinNextUp,
|
||||
type JellyfinItem
|
||||
} from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import {
|
||||
getPosterProps,
|
||||
getTmdbMovie,
|
||||
getTmdbPopularMovies,
|
||||
TmdbApiOpen
|
||||
} from '$lib/apis/tmdb/tmdbApi';
|
||||
import Carousel from '$lib/components/Carousel/Carousel.svelte';
|
||||
import CarouselPlaceholderItems from '$lib/components/Carousel/CarouselPlaceholderItems.svelte';
|
||||
import GenreCard from '$lib/components/GenreCard.svelte';
|
||||
import NetworkCard from '$lib/components/NetworkCard.svelte';
|
||||
import PersonCard from '$lib/components/PersonCard/PersonCard.svelte';
|
||||
import Poster from '$lib/components/Poster/Poster.svelte';
|
||||
import TitleShowcases from '$lib/components/TitleShowcase/TitleShowcasesContainer.svelte';
|
||||
import { genres, networks } from '$lib/discover';
|
||||
import { jellyfinItemsStore } from '$lib/stores/data.store';
|
||||
import { settings } from '$lib/stores/settings.store';
|
||||
import type { TitleType } from '$lib/types';
|
||||
import { formatDateToYearMonthDay } from '$lib/utils';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let continueWatchingVisible = true;
|
||||
|
||||
const tmdbPopularMoviesPromise = getTmdbPopularMovies()
|
||||
.then((movies) => Promise.all(movies.map((movie) => getTmdbMovie(movie.id || 0))))
|
||||
.then((movies) => movies.filter((m) => !!m).slice(0, 10));
|
||||
|
||||
let nextUpP = getJellyfinNextUp();
|
||||
let continueWatchingP = getJellyfinContinueWatching();
|
||||
|
||||
let nextUpProps = Promise.all([nextUpP, continueWatchingP])
|
||||
.then(([nextUp, continueWatching]) => [
|
||||
...(continueWatching || []),
|
||||
...(nextUp?.filter((i) => !continueWatching?.find((c) => c.SeriesId === i.SeriesId)) || [])
|
||||
])
|
||||
.then((items) =>
|
||||
Promise.all(
|
||||
items?.map(async (item) => {
|
||||
const parentSeries = await jellyfinItemsStore.promise.then((items) =>
|
||||
items.find((i) => i.Id === item.SeriesId)
|
||||
);
|
||||
|
||||
return {
|
||||
tmdbId: Number(item.ProviderIds?.Tmdb) || Number(parentSeries?.ProviderIds?.Tmdb) || 0,
|
||||
jellyfinId: item.Id,
|
||||
backdropUrl: getJellyfinBackdrop(item),
|
||||
title: item.Name || '',
|
||||
progress: item.UserData?.PlayedPercentage || undefined,
|
||||
runtime: item.RunTimeTicks ? item.RunTimeTicks / 10_000_000 / 60 : 0,
|
||||
...(item.Type === 'Movie'
|
||||
? {
|
||||
type: 'movie',
|
||||
subtitle: item.Genres?.join(', ') || ''
|
||||
}
|
||||
: {
|
||||
type: 'series',
|
||||
subtitle:
|
||||
(item?.IndexNumber && 'Episode ' + item.IndexNumber) ||
|
||||
item.Genres?.join(', ') ||
|
||||
''
|
||||
})
|
||||
} as const;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
nextUpProps.then((props) => {
|
||||
if (props.length === 0) {
|
||||
continueWatchingVisible = false;
|
||||
}
|
||||
});
|
||||
|
||||
let showcaseIndex = 0;
|
||||
|
||||
async function onNext() {
|
||||
showcaseIndex = (showcaseIndex + 1) % (await tmdbPopularMoviesPromise).length;
|
||||
}
|
||||
|
||||
async function onPrevious() {
|
||||
showcaseIndex =
|
||||
(showcaseIndex - 1 + (await tmdbPopularMoviesPromise).length) %
|
||||
(await tmdbPopularMoviesPromise).length;
|
||||
}
|
||||
|
||||
const jellyfinItemsPromise = new Promise<JellyfinItem[]>((resolve) => {
|
||||
jellyfinItemsStore.subscribe((data) => {
|
||||
if (data.loading) return;
|
||||
resolve(data.data || []);
|
||||
});
|
||||
});
|
||||
|
||||
const fetchCardProps = async (
|
||||
items: {
|
||||
name?: string;
|
||||
title?: string;
|
||||
id?: number;
|
||||
vote_average?: number;
|
||||
number_of_seasons?: number;
|
||||
first_air_date?: string;
|
||||
poster_path?: string;
|
||||
}[],
|
||||
type: TitleType | undefined = undefined
|
||||
): Promise<ComponentProps<Poster>[]> => {
|
||||
const filtered = $settings.discover.excludeLibraryItems
|
||||
? items.filter(
|
||||
async (item) =>
|
||||
!(await jellyfinItemsPromise).find((i) => i.ProviderIds?.Tmdb === String(item.id))
|
||||
)
|
||||
: items;
|
||||
|
||||
return Promise.all(filtered.map(async (item) => getPosterProps(item, type))).then((props) =>
|
||||
props.filter((p) => p.backdropUrl)
|
||||
);
|
||||
};
|
||||
|
||||
const trendingItemsPromise = TmdbApiOpen.get('/3/trending/all/{time_window}', {
|
||||
params: {
|
||||
path: {
|
||||
time_window: 'day'
|
||||
},
|
||||
query: {
|
||||
language: $settings.language
|
||||
}
|
||||
}
|
||||
}).then((res) => res.data?.results || []);
|
||||
|
||||
const fetchTrendingProps = () => trendingItemsPromise.then(fetchCardProps);
|
||||
|
||||
const fetchTrendingActorProps = () =>
|
||||
TmdbApiOpen.get('/3/trending/person/{time_window}', {
|
||||
params: {
|
||||
path: {
|
||||
time_window: 'week'
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.results || [])
|
||||
.then((actors) =>
|
||||
actors
|
||||
.filter((a) => a.profile_path)
|
||||
.map((actor) => ({
|
||||
tmdbId: actor.id || 0,
|
||||
backdropUri: actor.profile_path || '',
|
||||
name: actor.name || '',
|
||||
subtitle: actor.known_for_department || ''
|
||||
}))
|
||||
);
|
||||
|
||||
const fetchUpcomingMovies = () =>
|
||||
TmdbApiOpen.get('/3/discover/movie', {
|
||||
params: {
|
||||
query: {
|
||||
'primary_release_date.gte': formatDateToYearMonthDay(new Date()),
|
||||
sort_by: 'popularity.desc',
|
||||
language: $settings.language,
|
||||
region: $settings.discover.region,
|
||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.results || [])
|
||||
.then(fetchCardProps);
|
||||
|
||||
const fetchUpcomingSeries = () =>
|
||||
TmdbApiOpen.get('/3/discover/tv', {
|
||||
params: {
|
||||
query: {
|
||||
'first_air_date.gte': formatDateToYearMonthDay(new Date()),
|
||||
sort_by: 'popularity.desc',
|
||||
language: $settings.language,
|
||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.results || [])
|
||||
.then((i) => fetchCardProps(i, 'series'));
|
||||
|
||||
const fetchDigitalReleases = () =>
|
||||
TmdbApiOpen.get('/3/discover/movie', {
|
||||
params: {
|
||||
query: {
|
||||
with_release_type: 4,
|
||||
sort_by: 'popularity.desc',
|
||||
'release_date.lte': formatDateToYearMonthDay(new Date()),
|
||||
language: $settings.language,
|
||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
||||
// region: $settings.discover.region
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.results || [])
|
||||
.then(fetchCardProps);
|
||||
|
||||
const fetchNowStreaming = () =>
|
||||
TmdbApiOpen.get('/3/discover/tv', {
|
||||
params: {
|
||||
query: {
|
||||
'air_date.gte': formatDateToYearMonthDay(new Date()),
|
||||
'first_air_date.lte': formatDateToYearMonthDay(new Date()),
|
||||
sort_by: 'popularity.desc',
|
||||
language: $settings.language,
|
||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.results || [])
|
||||
.then((i) => fetchCardProps(i, 'series'));
|
||||
|
||||
function parseIncludedLanguages(includedLanguages: string) {
|
||||
return includedLanguages.replace(' ', '').split(',').join('|');
|
||||
}
|
||||
|
||||
const PADDING = 'px-4 lg:px-8 2xl:px-16';
|
||||
</script>
|
||||
|
||||
<TitleShowcases />
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-12 py-6 bg-stone-950"
|
||||
in:fade|global={{
|
||||
duration: $settings.animationDuration,
|
||||
delay: $settings.animationDuration
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.popularPeople')}
|
||||
</div>
|
||||
{#await fetchTrendingActorProps()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<PersonCard {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.upcomingMovies')}
|
||||
</div>
|
||||
{#await fetchUpcomingMovies()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Poster {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.upcomingSeries')}
|
||||
</div>
|
||||
{#await fetchUpcomingSeries()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Poster {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.genres')}
|
||||
</div>
|
||||
{#each Object.values(genres) as genre (genre.tmdbGenreId)}
|
||||
<GenreCard {genre} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.newDigitalReleases')}
|
||||
</div>
|
||||
{#await fetchDigitalReleases()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Poster {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.streamingNow')}
|
||||
</div>
|
||||
{#await fetchNowStreaming()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Poster {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.TVNetworks')}
|
||||
</div>
|
||||
{#each Object.values(networks) as network (network.tmdbNetworkId)}
|
||||
<NetworkCard {network} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
</div>
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Settings } from '$lib/entities/Settings';
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
return json(await Settings.get());
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const values = await request.json();
|
||||
return json(await Settings.set('default', values));
|
||||
};
|
||||
@@ -1,244 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { JellyfinItem } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { TmdbApiOpen, getPosterProps } from '$lib/apis/tmdb/tmdbApi';
|
||||
import Carousel from '$lib/components/Carousel/Carousel.svelte';
|
||||
import CarouselPlaceholderItems from '$lib/components/Carousel/CarouselPlaceholderItems.svelte';
|
||||
import GenreCard from '$lib/components/GenreCard.svelte';
|
||||
import NetworkCard from '$lib/components/NetworkCard.svelte';
|
||||
import PersonCard from '$lib/components/PersonCard/PersonCard.svelte';
|
||||
import Poster from '$lib/components/Poster/Poster.svelte';
|
||||
import { genres, networks } from '$lib/discover';
|
||||
import { jellyfinItemsStore } from '$lib/stores/data.store';
|
||||
import { settings } from '$lib/stores/settings.store';
|
||||
import type { TitleType } from '$lib/types';
|
||||
import { formatDateToYearMonthDay } from '$lib/utils';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const jellyfinItemsPromise = new Promise<JellyfinItem[]>((resolve) => {
|
||||
jellyfinItemsStore.subscribe((data) => {
|
||||
if (data.loading) return;
|
||||
resolve(data.data || []);
|
||||
});
|
||||
});
|
||||
|
||||
const fetchCardProps = async (
|
||||
items: {
|
||||
name?: string;
|
||||
title?: string;
|
||||
id?: number;
|
||||
vote_average?: number;
|
||||
number_of_seasons?: number;
|
||||
first_air_date?: string;
|
||||
poster_path?: string;
|
||||
}[],
|
||||
type: TitleType | undefined = undefined
|
||||
): Promise<ComponentProps<Poster>[]> => {
|
||||
const filtered = $settings.discover.excludeLibraryItems
|
||||
? items.filter(
|
||||
async (item) =>
|
||||
!(await jellyfinItemsPromise).find((i) => i.ProviderIds?.Tmdb === String(item.id))
|
||||
)
|
||||
: items;
|
||||
|
||||
return Promise.all(filtered.map(async (item) => getPosterProps(item, type))).then((props) =>
|
||||
props.filter((p) => p.backdropUrl)
|
||||
);
|
||||
};
|
||||
|
||||
const trendingItemsPromise = TmdbApiOpen.get('/3/trending/all/{time_window}', {
|
||||
params: {
|
||||
path: {
|
||||
time_window: 'day'
|
||||
},
|
||||
query: {
|
||||
language: $settings.language
|
||||
}
|
||||
}
|
||||
}).then((res) => res.data?.results || []);
|
||||
|
||||
const fetchTrendingProps = () => trendingItemsPromise.then(fetchCardProps);
|
||||
|
||||
const fetchTrendingActorProps = () =>
|
||||
TmdbApiOpen.get('/3/trending/person/{time_window}', {
|
||||
params: {
|
||||
path: {
|
||||
time_window: 'week'
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.results || [])
|
||||
.then((actors) =>
|
||||
actors
|
||||
.filter((a) => a.profile_path)
|
||||
.map((actor) => ({
|
||||
tmdbId: actor.id || 0,
|
||||
backdropUri: actor.profile_path || '',
|
||||
name: actor.name || '',
|
||||
subtitle: actor.known_for_department || ''
|
||||
}))
|
||||
);
|
||||
|
||||
const fetchUpcomingMovies = () =>
|
||||
TmdbApiOpen.get('/3/discover/movie', {
|
||||
params: {
|
||||
query: {
|
||||
'primary_release_date.gte': formatDateToYearMonthDay(new Date()),
|
||||
sort_by: 'popularity.desc',
|
||||
language: $settings.language,
|
||||
region: $settings.discover.region,
|
||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.results || [])
|
||||
.then(fetchCardProps);
|
||||
|
||||
const fetchUpcomingSeries = () =>
|
||||
TmdbApiOpen.get('/3/discover/tv', {
|
||||
params: {
|
||||
query: {
|
||||
'first_air_date.gte': formatDateToYearMonthDay(new Date()),
|
||||
sort_by: 'popularity.desc',
|
||||
language: $settings.language,
|
||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.results || [])
|
||||
.then((i) => fetchCardProps(i, 'series'));
|
||||
|
||||
const fetchDigitalReleases = () =>
|
||||
TmdbApiOpen.get('/3/discover/movie', {
|
||||
params: {
|
||||
query: {
|
||||
with_release_type: 4,
|
||||
sort_by: 'popularity.desc',
|
||||
'release_date.lte': formatDateToYearMonthDay(new Date()),
|
||||
language: $settings.language,
|
||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
||||
// region: $settings.discover.region
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.results || [])
|
||||
.then(fetchCardProps);
|
||||
|
||||
const fetchNowStreaming = () =>
|
||||
TmdbApiOpen.get('/3/discover/tv', {
|
||||
params: {
|
||||
query: {
|
||||
'air_date.gte': formatDateToYearMonthDay(new Date()),
|
||||
'first_air_date.lte': formatDateToYearMonthDay(new Date()),
|
||||
sort_by: 'popularity.desc',
|
||||
language: $settings.language,
|
||||
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.results || [])
|
||||
.then((i) => fetchCardProps(i, 'series'));
|
||||
|
||||
function parseIncludedLanguages(includedLanguages: string) {
|
||||
return includedLanguages.replace(' ', '').split(',').join('|');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- {#await trendingItemsPromise then items}
|
||||
{#if items.length}
|
||||
<div class="absolute inset-0 blur-3xl brightness-[0.2] z-[-1] scale-125">
|
||||
<LazyImg src={TMDB_IMAGES_ORIGINAL + items?.[4].backdrop_path} class="h-full" />
|
||||
</div>
|
||||
{/if}
|
||||
{/await} -->
|
||||
|
||||
<div
|
||||
class="pt-24 pb-8"
|
||||
in:fade|global={{
|
||||
duration: $settings.animationDuration,
|
||||
delay: $settings.animationDuration
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<div class="max-w-screen-2xl mx-auto">
|
||||
<Carousel
|
||||
gradientFromColor="from-stone-950"
|
||||
heading={$_('discover.trending')}
|
||||
class="mx-2 sm:mx-8 2xl:mx-0"
|
||||
>
|
||||
{#await fetchTrendingProps()}
|
||||
<CarouselPlaceholderItems size="lg" />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Poster {...prop} size="lg" />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-12 max-w-screen-2xl mx-auto py-4"
|
||||
in:fade|global={{
|
||||
duration: $settings.animationDuration,
|
||||
delay: $settings.animationDuration
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.popularPeople')}>
|
||||
{#await fetchTrendingActorProps()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<PersonCard {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.upcomingMovies')}>
|
||||
{#await fetchUpcomingMovies()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Poster {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.upcomingSeries')}>
|
||||
{#await fetchUpcomingSeries()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Poster {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.genres')}>
|
||||
{#each Object.values(genres) as genre (genre.tmdbGenreId)}
|
||||
<GenreCard {genre} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.newDigitalReleases')}>
|
||||
{#await fetchDigitalReleases()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Poster {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.streamingNow')}>
|
||||
{#await fetchNowStreaming()}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop (prop.tmdbId)}
|
||||
<Poster {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel class="mx-2 sm:mx-8 2xl:mx-0" heading={$_('discover.TVNetworks')}>
|
||||
{#each Object.values(networks) as network (network.tmdbNetworkId)}
|
||||
<NetworkCard {network} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
</div>
|
||||
@@ -1,69 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getTmdbGenreMovies } from '$lib/apis/tmdb/tmdbApi';
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
import CardPlaceholder from '$lib/components/Card/CardPlaceholder.svelte';
|
||||
import { fetchCardTmdbProps } from '$lib/components/Card/card';
|
||||
import GridPage from '$lib/components/GridPage/GridPage.svelte';
|
||||
import { genres, type Genre } from '$lib/discover';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const genre = genres[data.genre];
|
||||
|
||||
async function fetchGenreItems(genre: Genre) {
|
||||
const movies = await getTmdbGenreMovies(genre.tmdbGenreId);
|
||||
|
||||
const itemProps = await Promise.all(movies.map(fetchCardTmdbProps));
|
||||
|
||||
return {
|
||||
movies,
|
||||
itemProps
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<GridPage title={data.genre}>
|
||||
{#if genre}
|
||||
{#await fetchGenreItems(genre)}
|
||||
{#each [...Array(20).keys()] as index (index)}
|
||||
<CardPlaceholder size="dynamic" {index} />
|
||||
{/each}
|
||||
{:then { itemProps }}
|
||||
{#each itemProps as itemProps}
|
||||
<Card {...itemProps} size="dynamic" />
|
||||
{/each}
|
||||
{:catch error}
|
||||
{error.message}
|
||||
{/await}
|
||||
{:else}
|
||||
404
|
||||
{/if}
|
||||
</GridPage>
|
||||
|
||||
<!-- <div class="pt-24 p-8 bg-black">
|
||||
<button on:click={() => window?.history?.back()}>Back</button>
|
||||
{data.genre}
|
||||
</div>
|
||||
|
||||
<div class="p-8">
|
||||
{#if genre}
|
||||
{#await fetchGenreItems(genre)}
|
||||
<CardGrid>
|
||||
{#each [...Array(20).keys()] as index (index)}
|
||||
<CardPlaceholder size="dynamic" {index} />
|
||||
{/each}
|
||||
</CardGrid>
|
||||
{:then { itemProps }}
|
||||
<CardGrid>
|
||||
{#each itemProps as itemProps}
|
||||
<Card {...itemProps} size="dynamic" />
|
||||
{/each}
|
||||
</CardGrid>
|
||||
{:catch error}
|
||||
{error.message}
|
||||
{/await}
|
||||
{:else}
|
||||
404
|
||||
{/if}
|
||||
</div> -->
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
return {
|
||||
genre: params.genre
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,46 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getTmdbNetworkSeries } from '$lib/apis/tmdb/tmdbApi';
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
import CardGrid from '$lib/components/Card/CardGrid.svelte';
|
||||
import CardPlaceholder from '$lib/components/Card/CardPlaceholder.svelte';
|
||||
import { fetchCardTmdbSeriesProps } from '$lib/components/Card/card';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import GridPage from '$lib/components/GridPage/GridPage.svelte';
|
||||
import { networks, type Network } from '../../../../lib/discover';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const network = networks[data.network] || undefined;
|
||||
|
||||
async function fetchNetworkSeries(network: Network) {
|
||||
const shows = await getTmdbNetworkSeries(network.tmdbNetworkId);
|
||||
|
||||
const showProps: ComponentProps<Card>[] = await Promise.all(
|
||||
shows.map(fetchCardTmdbSeriesProps)
|
||||
);
|
||||
|
||||
return {
|
||||
shows,
|
||||
showProps
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<GridPage title={data.network}>
|
||||
{#if network}
|
||||
{#await fetchNetworkSeries(network)}
|
||||
{#each [...Array(20).keys()] as index (index)}
|
||||
<CardPlaceholder size="dynamic" {index} />
|
||||
{/each}
|
||||
{:then { showProps }}
|
||||
{#each showProps as showProps}
|
||||
<Card {...showProps} size="dynamic" />
|
||||
{/each}
|
||||
{:catch error}
|
||||
{error.message}
|
||||
{/await}
|
||||
{:else}
|
||||
404
|
||||
{/if}
|
||||
</GridPage>
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
return {
|
||||
network: params.network
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,173 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
getJellyfinBackdrop,
|
||||
getJellyfinPosterUrl,
|
||||
type JellyfinItem
|
||||
} from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Carousel from '$lib/components/Carousel/Carousel.svelte';
|
||||
import Poster from '$lib/components/Poster/Poster.svelte';
|
||||
import { playerState } from '$lib/components/VideoPlayer/VideoPlayer';
|
||||
import { PLACEHOLDER_BACKDROP } from '$lib/constants';
|
||||
import { jellyfinItemsStore, servarrDownloadsStore } from '$lib/stores/data.store';
|
||||
import { settings } from '$lib/stores/settings.store';
|
||||
import { ChevronRight } from 'radix-icons-svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LibraryItems from './LibraryItems.svelte';
|
||||
import { capitalize } from '$lib/utils';
|
||||
import LazyImg from '$lib/components/LazyImg.svelte';
|
||||
|
||||
let openNextUpTab: 'downloading' | 'nextUp' = 'downloading';
|
||||
let noItems = false;
|
||||
|
||||
let showcasePromise: Promise<JellyfinItem | undefined> = jellyfinItemsStore.promise.then(
|
||||
(items) =>
|
||||
items
|
||||
?.slice()
|
||||
?.sort((a, b) =>
|
||||
(a.DateCreated || a.DateLastMediaAdded || '') <
|
||||
(b.DateCreated || b.DateLastMediaAdded || '')
|
||||
? 1
|
||||
: -1
|
||||
)?.[3]
|
||||
);
|
||||
|
||||
let downloadProps: ComponentProps<Poster>[] = [];
|
||||
$: {
|
||||
const sonarrProps: ComponentProps<Poster>[] =
|
||||
$servarrDownloadsStore.sonarrDownloads?.map((item) => ({
|
||||
tvdbId: item.series.tvdbId,
|
||||
title: item.series.title || '',
|
||||
subtitle:
|
||||
`S${item.episode?.seasonNumber}E${item.episode?.episodeNumber} • ` +
|
||||
capitalize(item.status || ''),
|
||||
type: 'series',
|
||||
progress: 100 * (((item.size || 0) - (item.sizeleft || 0)) / (item.size || 1)),
|
||||
backdropUrl: item.series.images?.find((i) => i.coverType === 'poster')?.url || '',
|
||||
orientation: 'portrait'
|
||||
})) || [];
|
||||
|
||||
const radarrProps: ComponentProps<Poster>[] =
|
||||
$servarrDownloadsStore.radarrDownloads?.map((item) => ({
|
||||
tmdbId: item.movie.tmdbId,
|
||||
title: item.movie.title || '',
|
||||
subtitle: capitalize(item.status || ''),
|
||||
type: 'movie',
|
||||
backdropUrl: item.movie.images?.find((i) => i.coverType === 'poster')?.url || '',
|
||||
progress: 100 * (((item.size || 0) - (item.sizeleft || 0)) / (item.size || 1)),
|
||||
orientation: 'portrait'
|
||||
})) || [];
|
||||
|
||||
downloadProps = [...(sonarrProps || []), ...(radarrProps || [])];
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if noItems}
|
||||
<div
|
||||
class="h-screen flex items-center justify-center text-zinc-500 p-8"
|
||||
in:fade|global={{
|
||||
duration: $settings.animationDuration,
|
||||
delay: $settings.animationDuration
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<h1>
|
||||
{$_('library.missingConfiguration')}
|
||||
</h1>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
in:fade|global={{
|
||||
duration: $settings.animationDuration,
|
||||
delay: $settings.animationDuration
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<div class="relative pt-24">
|
||||
{#await showcasePromise then showcase}
|
||||
<LazyImg
|
||||
src={(showcase && getJellyfinBackdrop(showcase)) || PLACEHOLDER_BACKDROP}
|
||||
class="absolute inset-0"
|
||||
/>
|
||||
{/await}
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-stone-950 to-80% to-darken" />
|
||||
<div
|
||||
class="max-w-screen-2xl mx-auto relative z-[1] px-2 md:px-8 pt-32 xl:pt-56 pb-12 overflow-hidden"
|
||||
>
|
||||
<h1
|
||||
class="absolute font-bold uppercase text-amber-200 opacity-10 bottom-12 right-8 text-9xl hidden xl:block z-[-1]"
|
||||
>
|
||||
Library
|
||||
</h1>
|
||||
<div class="flex gap-4 items-end">
|
||||
{#await showcasePromise}
|
||||
<div class="w-32 aspect-[2/3] placeholder rounded-lg shadow-lg" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="placeholder-text w-20">Placeholder</div>
|
||||
<div class="placeholder-text w-[50vw] text-3xl sm:text-4xl md:text-5xl">
|
||||
Placeholder
|
||||
</div>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="placeholder-text w-28 h-10" />
|
||||
<div class="placeholder-text w-28 h-10" />
|
||||
</div>
|
||||
</div>
|
||||
{:then showcase}
|
||||
<div
|
||||
style={"background-image: url('" +
|
||||
(showcase ? getJellyfinPosterUrl(showcase) : '') +
|
||||
"');"}
|
||||
class="w-32 aspect-[2/3] rounded-lg bg-center bg-cover flex-shrink-0 shadow-lg"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-zinc-400 font-medium">Latest Addition</p>
|
||||
<h1 class="text-3xl sm:text-4xl md:text-5xl font-semibold">
|
||||
{showcase?.Name}
|
||||
</h1>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<Button
|
||||
type="primary"
|
||||
on:click={() => showcase?.Id && playerState.streamJellyfinId(showcase?.Id)}
|
||||
>
|
||||
Play<ChevronRight size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
href={`/${showcase?.Type === 'Movie' ? 'movie' : 'series'}/${
|
||||
showcase?.ProviderIds?.Tmdb || showcase?.ProviderIds?.Tvdb
|
||||
}`}
|
||||
>
|
||||
<span>{$_('titleShowcase.details')}</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="py-4 px-2 md:px-8"
|
||||
in:fade|global={{
|
||||
duration: $settings.animationDuration,
|
||||
delay: $settings.animationDuration
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<div class="max-w-screen-2xl m-auto flex flex-col gap-12">
|
||||
{#if downloadProps?.length}
|
||||
<div>
|
||||
<Carousel heading="Downloading">
|
||||
{#each downloadProps as props}
|
||||
<Poster {...props} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<LibraryItems />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,13 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import MoviePage from './MoviePage.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let tmdbId: number;
|
||||
$: tmdbId = Number(data.tmdbId);
|
||||
</script>
|
||||
|
||||
{#key tmdbId}
|
||||
<MoviePage {tmdbId} />
|
||||
{/key}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
return {
|
||||
tmdbId: params.id
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,13 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import PersonPage from './PersonPage.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let personId: number;
|
||||
$: personId = Number(data.personId);
|
||||
</script>
|
||||
|
||||
{#key personId}
|
||||
<PersonPage tmdbId={personId} />
|
||||
{/key}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
return {
|
||||
personId: Number(params.id)
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,14 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { TitleId } from '$lib/types';
|
||||
import type { PageData } from './$types';
|
||||
import SeriesPage from './SeriesPage.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let titleId: TitleId;
|
||||
$: titleId = { provider: 'tmdb', id: data.tmdbId, type: 'series' };
|
||||
</script>
|
||||
|
||||
{#key titleId}
|
||||
<SeriesPage {titleId} />
|
||||
{/key}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
return {
|
||||
tmdbId: Number(params.id)
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,283 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { version } from '$app/environment';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { getJellyfinHealth, getJellyfinUsers } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { getRadarrHealth } from '$lib/apis/radarr/radarrApi';
|
||||
import { getSonarrHealth } from '$lib/apis/sonarr/sonarrApi';
|
||||
import FormButton from '$lib/components/forms/FormButton.svelte';
|
||||
import Select from '$lib/components/forms/Select.svelte';
|
||||
import { settings, type SettingsValues } from '$lib/stores/settings.store';
|
||||
import axios from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import { ChevronLeft } from 'radix-icons-svelte';
|
||||
import GeneralSettingsPage from './GeneralSettingsPage.svelte';
|
||||
import IntegrationSettingsPage from './IntegrationSettingsPage.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { createErrorNotification } from '$lib/stores/notification.store';
|
||||
|
||||
type Section = 'general' | 'integrations';
|
||||
|
||||
let openTab: Section = 'general';
|
||||
|
||||
let sonarrConnected = false;
|
||||
let radarrConnected = false;
|
||||
let jellyfinConnected = false;
|
||||
|
||||
let values: SettingsValues;
|
||||
let initialValues: SettingsValues;
|
||||
settings.subscribe(async (v) => {
|
||||
values = structuredClone(v);
|
||||
initialValues = structuredClone(v);
|
||||
const s = updateSonarrHealth();
|
||||
const r = updateRadarrHealth();
|
||||
const j = updateJellyfinHealth();
|
||||
|
||||
await Promise.all([s, r, j]);
|
||||
|
||||
checkForPartialConfiguration(v);
|
||||
});
|
||||
|
||||
let valuesChanged = false;
|
||||
$: valuesChanged = JSON.stringify(initialValues) !== JSON.stringify(values);
|
||||
|
||||
let submitLoading = false;
|
||||
function handleSubmit() {
|
||||
if (submitLoading || !valuesChanged) return;
|
||||
submitLoading = true;
|
||||
submit().finally(() => (submitLoading = false));
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (
|
||||
values.sonarr.apiKey &&
|
||||
values.sonarr.baseUrl &&
|
||||
!(await getSonarrHealth(values.sonarr.baseUrl, values.sonarr.apiKey))
|
||||
) {
|
||||
createErrorNotification(
|
||||
'Invalid Configuration',
|
||||
'Could not connect to Sonarr. Check Sonarr credentials.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
values.radarr.apiKey &&
|
||||
values.radarr.baseUrl &&
|
||||
!(await getRadarrHealth(values.radarr.baseUrl, values.radarr.apiKey))
|
||||
) {
|
||||
createErrorNotification(
|
||||
'Invalid Configuration',
|
||||
'Could not connect to Radarr. Check Radarr credentials.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.jellyfin.apiKey && values.jellyfin.baseUrl) {
|
||||
if (!(await getJellyfinHealth(values.jellyfin.baseUrl, values.jellyfin.apiKey))) {
|
||||
createErrorNotification(
|
||||
'Invalid Configuration',
|
||||
'Could not connect to Jellyfin. Check Jellyfin credentials.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const users = await getJellyfinUsers(values.jellyfin.baseUrl, values.jellyfin.apiKey);
|
||||
if (!users.find((u) => u.Id === values.jellyfin.userId)) values.jellyfin.userId = null;
|
||||
}
|
||||
|
||||
updateSonarrHealth();
|
||||
updateRadarrHealth();
|
||||
updateJellyfinHealth();
|
||||
|
||||
axios.post('/api/settings', values).then(() => {
|
||||
settings.set(values);
|
||||
});
|
||||
}
|
||||
|
||||
function checkForPartialConfiguration(v: SettingsValues) {
|
||||
let error = '';
|
||||
if (sonarrConnected && !v.sonarr.rootFolderPath) {
|
||||
error = 'Sonarr disabled: Root folder path is required';
|
||||
} else if (sonarrConnected && !v.sonarr.qualityProfileId) {
|
||||
error = 'Sonarr disabled: Quality profile is required';
|
||||
} else if (sonarrConnected && !v.sonarr.languageProfileId) {
|
||||
error = 'Sonarr disabled: Language profile is required';
|
||||
}
|
||||
|
||||
if (radarrConnected && !v.radarr.rootFolderPath) {
|
||||
error = 'Radarr disabled: Root folder path is required';
|
||||
} else if (radarrConnected && !v.radarr.qualityProfileId) {
|
||||
error = 'Radarr disabled: Quality profile is required';
|
||||
}
|
||||
|
||||
if (jellyfinConnected && !v.jellyfin.userId) {
|
||||
error = 'Jellyfin disabled: User is required';
|
||||
}
|
||||
if (error) createErrorNotification('Incomplete Configuration', error, 'warning');
|
||||
}
|
||||
|
||||
async function updateSonarrHealth(reset = false): Promise<boolean> {
|
||||
if (!values.sonarr.baseUrl || !values.sonarr.apiKey || reset) {
|
||||
sonarrConnected = false;
|
||||
return false;
|
||||
}
|
||||
return getSonarrHealth(
|
||||
values.sonarr.baseUrl || undefined,
|
||||
values.sonarr.apiKey || undefined
|
||||
).then((ok) => {
|
||||
sonarrConnected = ok;
|
||||
return ok;
|
||||
});
|
||||
}
|
||||
|
||||
async function updateRadarrHealth(reset = false): Promise<boolean> {
|
||||
if (!values.radarr.baseUrl || !values.radarr.apiKey || reset) {
|
||||
radarrConnected = false;
|
||||
return false;
|
||||
}
|
||||
return getRadarrHealth(
|
||||
values.radarr.baseUrl || undefined,
|
||||
values.radarr.apiKey || undefined
|
||||
).then((ok) => {
|
||||
radarrConnected = ok;
|
||||
return ok;
|
||||
});
|
||||
}
|
||||
|
||||
async function updateJellyfinHealth(reset = false): Promise<boolean> {
|
||||
if (!values.jellyfin.baseUrl || !values.jellyfin.apiKey || reset) {
|
||||
jellyfinConnected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return getJellyfinHealth(
|
||||
values.jellyfin.baseUrl || undefined,
|
||||
values.jellyfin.apiKey || undefined
|
||||
).then((ok) => {
|
||||
jellyfinConnected = ok;
|
||||
return ok;
|
||||
});
|
||||
}
|
||||
|
||||
const getNavButtonStyle = (section: Section) =>
|
||||
classNames('rounded-xl p-2 px-6 font-medium text-left', {
|
||||
'text-zinc-200 bg-lighten': openTab === section,
|
||||
'text-zinc-300 hover:text-zinc-200': openTab !== section
|
||||
});
|
||||
|
||||
beforeNavigate(({ cancel }) => {
|
||||
if (valuesChanged) {
|
||||
if (!confirm('You have unsaved changes. Are you sure you want to leave?')) cancel();
|
||||
}
|
||||
});
|
||||
|
||||
function handleKeybinds(event: KeyboardEvent) {
|
||||
if (event.key === 's' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeybinds} />
|
||||
|
||||
<div
|
||||
class="min-h-screen sm:h-screen flex-1 flex flex-col sm:flex-row w-full sm:pt-24"
|
||||
in:fade|global={{
|
||||
duration: $settings.animationDuration,
|
||||
delay: $settings.animationDuration
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<div
|
||||
class="hidden sm:flex flex-col gap-2 border-r border-zinc-800 justify-between w-64 p-8 border-t"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
class="mb-6 text-lg font-medium flex items-center text-zinc-300 hover:text-zinc-200"
|
||||
on:click={() => history.back()}
|
||||
>
|
||||
<ChevronLeft size={22} />
|
||||
{$_('settings.navbar.settings')}
|
||||
</button>
|
||||
<button
|
||||
on:click={() => (openTab = 'general')}
|
||||
class={openTab && getNavButtonStyle('general')}
|
||||
>
|
||||
{$_('settings.navbar.general')}
|
||||
</button>
|
||||
<button
|
||||
on:click={() => (openTab = 'integrations')}
|
||||
class={openTab && getNavButtonStyle('integrations')}
|
||||
>
|
||||
{$_('settings.navbar.integrations')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<FormButton
|
||||
disabled={!valuesChanged}
|
||||
loading={submitLoading}
|
||||
on:click={handleSubmit}
|
||||
type={valuesChanged ? 'success' : 'base'}
|
||||
>
|
||||
{$_('settings.misc.saveChanges')}
|
||||
</FormButton>
|
||||
<FormButton
|
||||
disabled={!valuesChanged}
|
||||
type="error"
|
||||
on:click={() => {
|
||||
settings.set(initialValues);
|
||||
}}
|
||||
>
|
||||
{$_('settings.misc.resetToDefaults')}
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sm:hidden px-8 pt-20 pb-4 flex items-center justify-between">
|
||||
<button
|
||||
class="text-lg font-medium flex items-center text-zinc-300 hover:text-zinc-200"
|
||||
on:click={() => history.back()}
|
||||
>
|
||||
<ChevronLeft size={22} />
|
||||
{$_('settings.navbar.settings')}
|
||||
</button>
|
||||
<Select bind:value={openTab}>
|
||||
<option value="general"> {$_('settings.navbar.general')} </option>
|
||||
<option value="integrations">
|
||||
{$_('settings.navbar.integrations')}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-col border-t border-zinc-800 justify-between">
|
||||
<div class="overflow-y-scroll overflow-x-hidden px-8">
|
||||
<div class="max-w-screen-md mx-auto mb-auto w-full">
|
||||
{#if openTab === 'general'}
|
||||
<GeneralSettingsPage bind:values />
|
||||
{/if}
|
||||
|
||||
{#if openTab === 'integrations'}
|
||||
<IntegrationSettingsPage
|
||||
bind:values
|
||||
{sonarrConnected}
|
||||
{radarrConnected}
|
||||
{jellyfinConnected}
|
||||
{updateSonarrHealth}
|
||||
{updateRadarrHealth}
|
||||
{updateJellyfinHealth}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center p-4 gap-8 justify-center text-zinc-500 bg-stone-950">
|
||||
<div>v{version}</div>
|
||||
<a target="_blank" href="https://github.com/aleksilassila/reiverr/releases">
|
||||
{$_('settings.misc.changelog')}
|
||||
</a>
|
||||
<a target="_blank" href="https://github.com/aleksilassila/reiverr">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Language settings -->
|
||||
@@ -1,39 +0,0 @@
|
||||
<script lang="ts">
|
||||
import RadarrStats from '$lib/components/SourceStats/RadarrStats.svelte';
|
||||
import SonarrStats from '$lib/components/SourceStats/SonarrStats.svelte';
|
||||
import { settings } from '$lib/stores/settings.store';
|
||||
import { fade } from 'svelte/transition';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="pt-24 px-8 min-h-screen flex justify-center"
|
||||
in:fade|global={{
|
||||
duration: $settings.animationDuration,
|
||||
delay: $settings.animationDuration
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<div class="flex flex-col gap-4 max-w-3xl flex-1">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<RadarrStats large />
|
||||
<SonarrStats large />
|
||||
<!-- <div
|
||||
class="border-zinc-800 border-2 border-dashed relative w-full p-3 px-4 rounded-xl overflow-hidden text-zinc-500 text-center flex flex-col gap-1"
|
||||
>
|
||||
<h2 class="font-medium">Sonarr is not set up</h2>
|
||||
<p class="text-sm">
|
||||
To set up Sonarr, define the <code>PUBLIC_SONARR_API_KEY</code> and <code>SONARR_URL</code> environment
|
||||
variables.
|
||||
</p>
|
||||
</div> -->
|
||||
</div>
|
||||
<!-- <div>Sources</div>-->
|
||||
<!-- <div>Streaming services</div>-->
|
||||
<!-- <div>(P2P network?)</div>-->
|
||||
<!-- <div>Configure Sonarr</div>-->
|
||||
<!-- <div>Configure Radarr</div>-->
|
||||
<!-- <div>(Configure Plex)</div>-->
|
||||
<!-- <div>(Configure Jellyfinn)</div>-->
|
||||
<!-- <div>(Configure IMDB)</div>-->
|
||||
<!-- <div>(Configure TMDB)</div>-->
|
||||
</div>
|
||||
2
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
27
svelte.config.js
Executable file → Normal file
@@ -1,5 +1,4 @@
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -7,28 +6,10 @@ const file = fileURLToPath(new URL('package.json', import.meta.url));
|
||||
const json = readFileSync(file, 'utf8');
|
||||
const pkg = JSON.parse(json);
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter({
|
||||
fallback: 'index.html'
|
||||
}),
|
||||
paths: {
|
||||
assets: 'https://replaceme.com'
|
||||
},
|
||||
|
||||
version: {
|
||||
name: pkg.version
|
||||
}
|
||||
},
|
||||
|
||||
vitePlugin: {
|
||||
inspector: {
|
||||
// toggleKeyCombo: 'meta',
|
||||
@@ -36,5 +17,3 @@ const config = {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"lib": ["es6"],
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"strictPropertyInitialization": false
|
||||
}
|
||||
// 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
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
9
tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
// import * as pkg from './package.json';
|
||||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||
import viteLegacyPlugin from '@vitejs/plugin-legacy';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
// define: {
|
||||
// PKG: pkg
|
||||
plugins: [viteLegacyPlugin(), svelte(), viteSingleFile()]
|
||||
|
||||
// base: '/dist',
|
||||
// experimental: {
|
||||
// renderBuiltUrl() {
|
||||
// return { relative: true }
|
||||
// }
|
||||
// },
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
},
|
||||
ssr: {
|
||||
external: ['reflect-metadata']
|
||||
}
|
||||
});
|
||||
|
||||