feat: Update checker
This commit is contained in:
@@ -2,14 +2,19 @@
|
||||
import I18n from './lib/components/Lang/I18n.svelte';
|
||||
import { appState } from './lib/stores/app-state.store';
|
||||
import { handleKeyboardNavigation } from './lib/selectable';
|
||||
import Container from './Container.svelte';
|
||||
import LoginPage from './lib/pages/LoginPage.svelte';
|
||||
import ModalStack from './lib/components/Modal/ModalStack.svelte';
|
||||
import NavigationDebugger from './lib/components/DebugElements.svelte';
|
||||
import StackRouter from './lib/components/StackRouter/StackRouter.svelte';
|
||||
import { defaultStackRouter } from './lib/components/StackRouter/StackRouter';
|
||||
import Sidebar from './lib/components/Sidebar/Sidebar.svelte';
|
||||
import OnboardingPage from './lib/pages/OnboardingPage.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { skippedVersion } from './lib/stores/localstorage.store';
|
||||
import axios from 'axios';
|
||||
import NotificationStack from './lib/components/Notifications/NotificationStack.svelte';
|
||||
import { createModal } from './lib/components/Modal/modal.store';
|
||||
import UpdateDialog from './lib/components/Dialog/UpdateDialog.svelte';
|
||||
import { localSettings } from './lib/stores/localstorage.store';
|
||||
|
||||
appState.subscribe((s) => console.log('appState', s));
|
||||
|
||||
@@ -27,6 +32,22 @@
|
||||
// tizen.mediakey.setMediaKeyEventListener(myMediaKeyChangeListener);
|
||||
// }
|
||||
// });
|
||||
|
||||
async function fetchLatestVersion() {
|
||||
return axios
|
||||
.get('https://api.github.com/repos/aleksilassila/reiverr/tags')
|
||||
.then((res) => res.data?.find((v: { name: string }) => v.name.startsWith('v2'))?.name);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ($localSettings.checkForUpdates)
|
||||
fetchLatestVersion().then((latestVersion) => {
|
||||
// @ts-ignore
|
||||
if (latestVersion !== `v${VERSION}` && latestVersion !== $localSettings.skippedVersion) {
|
||||
createModal(UpdateDialog, { version: latestVersion });
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<I18n />
|
||||
@@ -70,7 +91,8 @@
|
||||
|
||||
<ModalStack />
|
||||
{/if}
|
||||
<!--</Container>-->
|
||||
|
||||
<NotificationStack />
|
||||
|
||||
<NavigationDebugger />
|
||||
|
||||
|
||||
44
src/lib/components/Dialog/UpdateDialog.svelte
Normal file
44
src/lib/components/Dialog/UpdateDialog.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import Dialog from './Dialog.svelte';
|
||||
import Container from '../../../Container.svelte';
|
||||
import Button from '../Button.svelte';
|
||||
import { PLATFORM_WEB } from '../../constants';
|
||||
import { ExternalLink, InfoCircled } from 'radix-icons-svelte';
|
||||
import { modalStack } from '../Modal/modal.store';
|
||||
import { localSettings } from '../../stores/localstorage.store';
|
||||
|
||||
export let version: string;
|
||||
|
||||
function disableUpdateChecking() {
|
||||
localSettings.update((s) => ({ ...s, checkForUpdates: false, skippedVersion: version }));
|
||||
modalStack.closeTopmost();
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
localSettings.update((s) => ({ ...s, skippedVersion: version }));
|
||||
modalStack.closeTopmost();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog>
|
||||
<div class="flex items-center justify-center text-secondary-500 mb-4">
|
||||
<InfoCircled size={64} />
|
||||
</div>
|
||||
<h1 class="header2 text-center">Update Available</h1>
|
||||
<div class="header1 mb-8 text-center">Reiverr {version} is now available.</div>
|
||||
<Container class="space-y-4">
|
||||
<Button type="primary-dark" on:clickOrSelect={dismiss}>Dismiss</Button>
|
||||
{#if PLATFORM_WEB}
|
||||
<Button
|
||||
type="primary-dark"
|
||||
iconAfter={ExternalLink}
|
||||
on:clickOrSelect={() => window.open('https://github.com/aleksilassila/reiverr/releases')}
|
||||
>
|
||||
Open Releases
|
||||
</Button>
|
||||
{/if}
|
||||
<Button type="primary-dark" on:clickOrSelect={disableUpdateChecking}>
|
||||
Disable Update Checking
|
||||
</Button>
|
||||
</Container>
|
||||
</Dialog>
|
||||
@@ -1,18 +1,14 @@
|
||||
import type { ComponentType, SvelteComponentTyped } from 'svelte';
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
import SeasonMediaManagerModal from '../MediaManagerModal/SeasonMediaManagerModal.svelte';
|
||||
import EpisodeMediaManagerModal from '../MediaManagerModal/EpisodeMediaManagerModal.svelte';
|
||||
import MovieMediaManagerModal from '../MediaManagerModal/MovieMediaManagerModal.svelte';
|
||||
import ConfirmDialog from '../Dialog/ConfirmDialog.svelte';
|
||||
import { sonarrApi, type SonarrSeries } from '../../apis/sonarr/sonarr-api';
|
||||
|
||||
type ModalItem = {
|
||||
id: symbol;
|
||||
group: symbol;
|
||||
component: ConstructorOfATypedSvelteComponent;
|
||||
component: ComponentType;
|
||||
props: Record<string, any>;
|
||||
};
|
||||
function createModalStack() {
|
||||
|
||||
export function createModalStack() {
|
||||
const items = writable<ModalItem[]>([]);
|
||||
const top = derived(items, ($items) => $items[$items.length - 1]);
|
||||
|
||||
@@ -62,22 +58,3 @@ function createModalStack() {
|
||||
export const modalStack = createModalStack();
|
||||
export const modalStackTop = modalStack.top;
|
||||
export const createModal = modalStack.create;
|
||||
|
||||
export const openSeasonMediaManager = (sonarrItem: SonarrSeries, season: number) =>
|
||||
modalStack.create(SeasonMediaManagerModal, { sonarrItem, season });
|
||||
|
||||
export const openEpisodeMediaManager = (tmdbId: number, season: number, episode: number) =>
|
||||
modalStack.create(EpisodeMediaManagerModal, { id: tmdbId, season, episode });
|
||||
|
||||
export const openMovieMediaManager = (tmdbId: number) =>
|
||||
modalStack.create(MovieMediaManagerModal, { id: tmdbId });
|
||||
|
||||
// let lastTitleModal: symbol | undefined = undefined;
|
||||
// export function openTitleModal(titleId: TitleId) {
|
||||
// if (lastTitleModal) {
|
||||
// modalStack.close(lastTitleModal);
|
||||
// }
|
||||
// lastTitleModal = modalStack.create(TitlePageModal, {
|
||||
// titleId
|
||||
// });
|
||||
// }
|
||||
|
||||
34
src/lib/components/Notifications/Notification.svelte
Normal file
34
src/lib/components/Notifications/Notification.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let duration = 5000;
|
||||
export let title: string;
|
||||
export let body = '';
|
||||
export let type: 'info' | 'warning' | 'error' = 'info';
|
||||
export let persistent = false;
|
||||
|
||||
let visible = true;
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
onMount(() => {
|
||||
timeout = setTimeout(() => {
|
||||
visible = false;
|
||||
}, duration);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if visible && !persistent}
|
||||
<div class={classNames('bg-primary-800 rounded-xl px-6 py-2 w-80', {})}>
|
||||
{#if !body}
|
||||
<h1>{title}</h1>
|
||||
{:else}
|
||||
<div>
|
||||
<h1>{title}</h1>
|
||||
<div>{body}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
27
src/lib/components/Notifications/NotificationStack.svelte
Normal file
27
src/lib/components/Notifications/NotificationStack.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly, fade } from 'svelte/transition';
|
||||
import { notificationStack } from './notification.store';
|
||||
|
||||
export let persistent = false;
|
||||
</script>
|
||||
|
||||
<div class="fixed top-8 right-8 z-50 flex flex-col">
|
||||
{#each $notificationStack
|
||||
.slice(Math.max($notificationStack.length - 5, 0))
|
||||
.reverse() as notification (notification.id)}
|
||||
<div
|
||||
animate:flip={{ duration: 500 }}
|
||||
in:fly|global={{ duration: 150, x: 50 }}
|
||||
out:fade|global={{ duration: 150 }}
|
||||
class="mb-4"
|
||||
>
|
||||
<svelte:component
|
||||
this={notification.component}
|
||||
id={notification.id}
|
||||
{...notification.props}
|
||||
{persistent}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
26
src/lib/components/Notifications/notification.store.ts
Normal file
26
src/lib/components/Notifications/notification.store.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { ComponentType } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
type NotificationItem = {
|
||||
id: symbol;
|
||||
component: ComponentType;
|
||||
props: Record<string, any>;
|
||||
};
|
||||
|
||||
function useNotificationStack() {
|
||||
const notifications = writable<NotificationItem[]>([]);
|
||||
|
||||
function create(component: NotificationItem['component'], props: NotificationItem['props'] = {}) {
|
||||
const id = Symbol();
|
||||
const item = { id, component, props };
|
||||
notifications.update((prev) => [...prev, item]);
|
||||
return id;
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: notifications.subscribe,
|
||||
create
|
||||
};
|
||||
}
|
||||
|
||||
export const notificationStack = useNotificationStack();
|
||||
33
src/lib/components/UpdateChecker.svelte
Normal file
33
src/lib/components/UpdateChecker.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { Cross2 } from 'radix-icons-svelte';
|
||||
import IconButton from './IconButton.svelte';
|
||||
import axios from 'axios';
|
||||
import Button from './Button.svelte';
|
||||
import { skippedVersion } from '../stores/localstorage.store';
|
||||
|
||||
let visible = true;
|
||||
|
||||
async function fetchLatestVersion() {
|
||||
return axios
|
||||
.get('https://api.github.com/repos/aleksilassila/reiverr/tags')
|
||||
.then((res) => res.data?.[0]?.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await fetchLatestVersion() then latestVersion}
|
||||
{#if latestVersion !== `v${VERSION}` && latestVersion !== $skippedVersion && visible}
|
||||
<div
|
||||
class="fixed inset-x-0 bottom-0 p-3 flex items-center justify-center z-20 bg-stone-800 text-sm"
|
||||
>
|
||||
<a href="https://github.com/aleksilassila/reiverr">{latestVersion} is now available!</a>
|
||||
<div class="absolute right-4 inset-y-0 flex items-center gap-2">
|
||||
<Button type="tertiary" size="xs" on:click={() => skippedVersion.set(latestVersion)}>
|
||||
Skip this version
|
||||
</Button>
|
||||
<IconButton on:click={() => (visible = false)}>
|
||||
<Cross2 size={20} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
@@ -1,159 +0,0 @@
|
||||
import { defaultSettings, type SettingsValues } from '$lib/stores/settings.store';
|
||||
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'settings' })
|
||||
export class Settings extends BaseEntity {
|
||||
@PrimaryColumn('text')
|
||||
name: string;
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isSetupDone: boolean;
|
||||
|
||||
// General
|
||||
|
||||
@Column('boolean', { default: defaultSettings.autoplayTrailers })
|
||||
autoplayTrailers: boolean;
|
||||
|
||||
@Column('text', { default: defaultSettings.language })
|
||||
language: string;
|
||||
|
||||
@Column('integer', { default: defaultSettings.animationDuration })
|
||||
animationDuration: number;
|
||||
|
||||
// Discover
|
||||
@Column('text', { default: defaultSettings.discover.region })
|
||||
discoverRegion: string;
|
||||
|
||||
@Column('boolean', { default: defaultSettings.discover.excludeLibraryItems })
|
||||
discoverExcludeLibraryItems: boolean;
|
||||
|
||||
@Column('text', { default: defaultSettings.discover.includedLanguages })
|
||||
discoverIncludedLanguages: string;
|
||||
|
||||
// Sonarr
|
||||
|
||||
@Column('text', { nullable: true, default: defaultSettings.sonarr.baseUrl })
|
||||
sonarrBaseUrl: string | null;
|
||||
|
||||
@Column('text', { nullable: true, default: defaultSettings.sonarr.apiKey })
|
||||
sonarrApiKey: string | null;
|
||||
|
||||
@Column('text', { default: defaultSettings.sonarr.rootFolderPath })
|
||||
sonarrRootFolderPath: string;
|
||||
|
||||
@Column('integer', { default: defaultSettings.sonarr.qualityProfileId })
|
||||
sonarrQualityProfileId: number;
|
||||
|
||||
@Column('integer', { default: defaultSettings.sonarr.languageProfileId })
|
||||
sonarrLanguageProfileId: number;
|
||||
|
||||
// Radarr
|
||||
|
||||
@Column('text', { nullable: true, default: defaultSettings.radarr.baseUrl })
|
||||
radarrBaseUrl: string | null;
|
||||
|
||||
@Column('text', { nullable: true, default: defaultSettings.radarr.apiKey })
|
||||
radarrApiKey: string | null;
|
||||
|
||||
@Column('text', { default: defaultSettings.radarr.rootFolderPath })
|
||||
radarrRootFolderPath: string;
|
||||
|
||||
@Column('integer', { default: defaultSettings.radarr.qualityProfileId })
|
||||
radarrQualityProfileId: number;
|
||||
|
||||
// Jellyfin
|
||||
|
||||
@Column('text', { nullable: true, default: defaultSettings.jellyfin.baseUrl })
|
||||
jellyfinBaseUrl: string | null;
|
||||
|
||||
@Column('text', { nullable: true, default: defaultSettings.jellyfin.apiKey })
|
||||
jellyfinApiKey: string | null;
|
||||
|
||||
@Column('text', { nullable: true, default: defaultSettings.jellyfin.userId })
|
||||
jellyfinUserId: string | null;
|
||||
|
||||
public static async get(name = 'default'): Promise<SettingsValues> {
|
||||
const settings = await this.findOne({ where: { name } });
|
||||
|
||||
if (!settings) {
|
||||
const defaultSettings = new Settings();
|
||||
defaultSettings.name = 'default';
|
||||
await defaultSettings.save();
|
||||
return this.getSettingsValues(defaultSettings);
|
||||
}
|
||||
|
||||
return this.getSettingsValues(settings);
|
||||
}
|
||||
|
||||
static getSettingsValues(settings: Settings): SettingsValues {
|
||||
return {
|
||||
...defaultSettings,
|
||||
language: settings.language,
|
||||
autoplayTrailers: settings.autoplayTrailers,
|
||||
animationDuration: settings.animationDuration,
|
||||
|
||||
discover: {
|
||||
...defaultSettings.discover,
|
||||
region: settings.discoverRegion,
|
||||
excludeLibraryItems: settings.discoverExcludeLibraryItems,
|
||||
includedLanguages: settings.discoverIncludedLanguages
|
||||
},
|
||||
|
||||
sonarr: {
|
||||
...defaultSettings.sonarr,
|
||||
apiKey: settings.sonarrApiKey,
|
||||
baseUrl: settings.sonarrBaseUrl,
|
||||
languageProfileId: settings.sonarrLanguageProfileId,
|
||||
qualityProfileId: settings.sonarrQualityProfileId,
|
||||
rootFolderPath: settings.sonarrRootFolderPath
|
||||
},
|
||||
radarr: {
|
||||
...defaultSettings.radarr,
|
||||
apiKey: settings.radarrApiKey,
|
||||
baseUrl: settings.radarrBaseUrl,
|
||||
qualityProfileId: settings.radarrQualityProfileId,
|
||||
rootFolderPath: settings.radarrRootFolderPath
|
||||
},
|
||||
jellyfin: {
|
||||
...defaultSettings.jellyfin,
|
||||
apiKey: settings.jellyfinApiKey,
|
||||
baseUrl: settings.jellyfinBaseUrl,
|
||||
userId: settings.jellyfinUserId
|
||||
},
|
||||
initialised: true
|
||||
};
|
||||
}
|
||||
|
||||
public static async set(name: string, values: SettingsValues): Promise<Settings | null> {
|
||||
const settings = await this.findOne({ where: { name } });
|
||||
|
||||
if (!settings) return null;
|
||||
|
||||
settings.language = values.language;
|
||||
settings.autoplayTrailers = values.autoplayTrailers;
|
||||
settings.animationDuration = values.animationDuration;
|
||||
|
||||
settings.discoverRegion = values.discover.region;
|
||||
settings.discoverExcludeLibraryItems = values.discover.excludeLibraryItems;
|
||||
settings.discoverIncludedLanguages = values.discover.includedLanguages;
|
||||
|
||||
settings.sonarrApiKey = values.sonarr.apiKey;
|
||||
settings.sonarrBaseUrl = values.sonarr.baseUrl;
|
||||
settings.sonarrLanguageProfileId = values.sonarr.languageProfileId;
|
||||
settings.sonarrQualityProfileId = values.sonarr.qualityProfileId;
|
||||
settings.sonarrRootFolderPath = values.sonarr.rootFolderPath;
|
||||
|
||||
settings.radarrApiKey = values.radarr.apiKey;
|
||||
settings.radarrBaseUrl = values.radarr.baseUrl;
|
||||
settings.radarrQualityProfileId = values.radarr.qualityProfileId;
|
||||
settings.radarrRootFolderPath = values.radarr.rootFolderPath;
|
||||
|
||||
settings.jellyfinApiKey = values.jellyfin.apiKey;
|
||||
settings.jellyfinBaseUrl = values.jellyfin.baseUrl;
|
||||
settings.jellyfinUserId = values.jellyfin.userId;
|
||||
|
||||
await settings.save();
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,15 @@
|
||||
import Container from '../../Container.svelte';
|
||||
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
||||
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
||||
import { useActionRequest, useDependantRequest, useRequest } from '../stores/data.store';
|
||||
import { useActionRequest } from '../stores/data.store';
|
||||
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../constants';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Check,
|
||||
DotFilled,
|
||||
Download,
|
||||
ExternalLink,
|
||||
File,
|
||||
Play,
|
||||
Plus,
|
||||
Trash
|
||||
} from 'radix-icons-svelte';
|
||||
import { Check, DotFilled, ExternalLink, Play, Plus, Trash } from 'radix-icons-svelte';
|
||||
import HeroInfoTitle from '../components/HeroInfo/HeroInfoTitle.svelte';
|
||||
import Button from '../components/Button.svelte';
|
||||
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
||||
import { playerState } from '../components/VideoPlayer/VideoPlayer';
|
||||
import { formatSize, timeout } from '../utils';
|
||||
import { tick } from 'svelte';
|
||||
import { createModal, openEpisodeMediaManager } from '../components/Modal/modal.store';
|
||||
import { formatSize } from '../utils';
|
||||
import { createModal } from '../components/Modal/modal.store';
|
||||
import ButtonGhost from '../components/Ghosts/ButtonGhost.svelte';
|
||||
import {
|
||||
type EpisodeFileResource,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { type MovieDownload, type MovieFileResource, radarrApi } from '../apis/radarr/radarr-api';
|
||||
import { useActionRequests, useRequest } from '../stores/data.store';
|
||||
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
||||
import { createModal, modalStack, openMovieMediaManager } from '../components/Modal/modal.store';
|
||||
import { createModal, modalStack } from '../components/Modal/modal.store';
|
||||
import { playerState } from '../components/VideoPlayer/VideoPlayer';
|
||||
import { scrollIntoView } from '../selectable';
|
||||
import Carousel from '../components/Carousel/Carousel.svelte';
|
||||
|
||||
@@ -22,7 +22,7 @@ export function createLocalStorageStore<T>(key: string, defaultValue: T) {
|
||||
};
|
||||
}
|
||||
|
||||
export const skippedVersion = createLocalStorageStore<string | null>('skipped-version', null);
|
||||
export const skippedVersion = createLocalStorageStore<string>('skipped-version', '');
|
||||
export const videoPlayerSettings = createLocalStorageStore<{
|
||||
muted: boolean;
|
||||
volume: number;
|
||||
@@ -33,7 +33,11 @@ export const videoPlayerSettings = createLocalStorageStore<{
|
||||
export const localSettings = createLocalStorageStore<{
|
||||
animateScrolling: boolean;
|
||||
useCssTransitions: boolean;
|
||||
checkForUpdates: boolean;
|
||||
skippedVersion: string;
|
||||
}>('settings', {
|
||||
animateScrolling: true,
|
||||
useCssTransitions: true
|
||||
useCssTransitions: true,
|
||||
checkForUpdates: true,
|
||||
skippedVersion: ''
|
||||
});
|
||||
|
||||
@@ -2,6 +2,12 @@ import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||
import viteLegacyPlugin from '@vitejs/plugin-legacy';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const file = fileURLToPath(new URL('package.json', import.meta.url));
|
||||
const json = readFileSync(file, 'utf8');
|
||||
const pkg = JSON.parse(json);
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -16,7 +22,10 @@ export default defineConfig({
|
||||
svelte(),
|
||||
viteSingleFile()
|
||||
],
|
||||
optimizeDeps: { exclude: ['svelte-navigator'] }
|
||||
optimizeDeps: { exclude: ['svelte-navigator'] },
|
||||
define: {
|
||||
VERSION: `"${pkg.version}"`
|
||||
}
|
||||
|
||||
// base: '/dist',
|
||||
// experimental: {
|
||||
|
||||
Reference in New Issue
Block a user