From 052ea44548c3decd9523dbaae616656c765fdcd2 Mon Sep 17 00:00:00 2001 From: Aleksi Lassila Date: Sat, 1 Jun 2024 14:09:12 +0300 Subject: [PATCH] feat: Update checker --- src/App.svelte | 28 ++- src/lib/components/Dialog/UpdateDialog.svelte | 44 +++++ src/lib/components/Modal/modal.store.ts | 29 +--- .../Notifications/Notification.svelte | 34 ++++ .../Notifications/NotificationStack.svelte | 27 +++ .../Notifications/notification.store.ts | 26 +++ src/lib/components/UpdateChecker.svelte | 33 ++++ src/lib/entities/Settings.ts | 159 ------------------ src/lib/pages/EpisodePage.svelte | 19 +-- src/lib/pages/MoviePage.svelte | 2 +- src/lib/stores/localstorage.store.ts | 8 +- vite.config.ts | 11 +- 12 files changed, 213 insertions(+), 207 deletions(-) create mode 100644 src/lib/components/Dialog/UpdateDialog.svelte create mode 100644 src/lib/components/Notifications/Notification.svelte create mode 100644 src/lib/components/Notifications/NotificationStack.svelte create mode 100644 src/lib/components/Notifications/notification.store.ts create mode 100644 src/lib/components/UpdateChecker.svelte delete mode 100644 src/lib/entities/Settings.ts diff --git a/src/App.svelte b/src/App.svelte index e629d5a..8bc6a1d 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -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 }); + } + }); + }); @@ -70,7 +91,8 @@ {/if} - + + diff --git a/src/lib/components/Dialog/UpdateDialog.svelte b/src/lib/components/Dialog/UpdateDialog.svelte new file mode 100644 index 0000000..b60d645 --- /dev/null +++ b/src/lib/components/Dialog/UpdateDialog.svelte @@ -0,0 +1,44 @@ + + + +
+ +
+

Update Available

+
Reiverr {version} is now available.
+ + + {#if PLATFORM_WEB} + + {/if} + + +
diff --git a/src/lib/components/Modal/modal.store.ts b/src/lib/components/Modal/modal.store.ts index 02e1616..e52717f 100644 --- a/src/lib/components/Modal/modal.store.ts +++ b/src/lib/components/Modal/modal.store.ts @@ -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; }; -function createModalStack() { + +export function createModalStack() { const items = writable([]); 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 -// }); -// } diff --git a/src/lib/components/Notifications/Notification.svelte b/src/lib/components/Notifications/Notification.svelte new file mode 100644 index 0000000..2788b23 --- /dev/null +++ b/src/lib/components/Notifications/Notification.svelte @@ -0,0 +1,34 @@ + + +{#if visible && !persistent} +
+ {#if !body} +

{title}

+ {:else} +
+

{title}

+
{body}
+
+ {/if} +
+{/if} diff --git a/src/lib/components/Notifications/NotificationStack.svelte b/src/lib/components/Notifications/NotificationStack.svelte new file mode 100644 index 0000000..1805d7f --- /dev/null +++ b/src/lib/components/Notifications/NotificationStack.svelte @@ -0,0 +1,27 @@ + + +
+ {#each $notificationStack + .slice(Math.max($notificationStack.length - 5, 0)) + .reverse() as notification (notification.id)} +
+ +
+ {/each} +
diff --git a/src/lib/components/Notifications/notification.store.ts b/src/lib/components/Notifications/notification.store.ts new file mode 100644 index 0000000..967b8bd --- /dev/null +++ b/src/lib/components/Notifications/notification.store.ts @@ -0,0 +1,26 @@ +import type { ComponentType } from 'svelte'; +import { writable } from 'svelte/store'; + +type NotificationItem = { + id: symbol; + component: ComponentType; + props: Record; +}; + +function useNotificationStack() { + const notifications = writable([]); + + 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(); diff --git a/src/lib/components/UpdateChecker.svelte b/src/lib/components/UpdateChecker.svelte new file mode 100644 index 0000000..80ae5e5 --- /dev/null +++ b/src/lib/components/UpdateChecker.svelte @@ -0,0 +1,33 @@ + + +{#await fetchLatestVersion() then latestVersion} + {#if latestVersion !== `v${VERSION}` && latestVersion !== $skippedVersion && visible} +
+ {latestVersion} is now available! +
+ + (visible = false)}> + + +
+
+ {/if} +{/await} diff --git a/src/lib/entities/Settings.ts b/src/lib/entities/Settings.ts deleted file mode 100644 index 6d8ef44..0000000 --- a/src/lib/entities/Settings.ts +++ /dev/null @@ -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 { - 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 { - 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; - } -} diff --git a/src/lib/pages/EpisodePage.svelte b/src/lib/pages/EpisodePage.svelte index f3d562d..f49c128 100644 --- a/src/lib/pages/EpisodePage.svelte +++ b/src/lib/pages/EpisodePage.svelte @@ -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, diff --git a/src/lib/pages/MoviePage.svelte b/src/lib/pages/MoviePage.svelte index 0633398..7faae2e 100644 --- a/src/lib/pages/MoviePage.svelte +++ b/src/lib/pages/MoviePage.svelte @@ -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'; diff --git a/src/lib/stores/localstorage.store.ts b/src/lib/stores/localstorage.store.ts index 8b3a45e..95af4a1 100644 --- a/src/lib/stores/localstorage.store.ts +++ b/src/lib/stores/localstorage.store.ts @@ -22,7 +22,7 @@ export function createLocalStorageStore(key: string, defaultValue: T) { }; } -export const skippedVersion = createLocalStorageStore('skipped-version', null); +export const skippedVersion = createLocalStorageStore('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: '' }); diff --git a/vite.config.ts b/vite.config.ts index 1d6601e..288cea9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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: {