feat: Update checker

This commit is contained in:
Aleksi Lassila
2024-06-01 14:09:12 +03:00
parent f6b9ac41ba
commit 052ea44548
12 changed files with 213 additions and 207 deletions

View File

@@ -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 />

View 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>

View File

@@ -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
// });
// }

View 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}

View 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>

View 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();

View 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}

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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';

View File

@@ -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: ''
});

View File

@@ -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: {