From e4ff6c4028c33feac03bb32bbd0eda84d8138a8f Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Sat, 20 Dec 2025 12:20:32 -0600 Subject: replace original client with static sveltekit site. Transition and refactor code for sveltekit. Add support for view transitions api. --- src/client/index.html | 138 ------------------- src/client/src/albums.js | 87 ------------ src/client/src/app.html | 12 ++ src/client/src/connector.js | 92 ------------- src/client/src/icons.js | 19 --- ...ched_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 - ...mera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg | 1 - ...rcle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg | 1 - ...lect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 - ...load_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg | 1 - ...mage_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 - ..._new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 - ...ause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 - ...lbum_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 - ...rame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 - ...rrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 - src/client/src/icons/removefill.sh | 6 - ...arch_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 - ..._all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 - ...ings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 - ...hare_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg | 1 - ...next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 - ...ious_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 - ...show_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 - ..._off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg | 1 - src/client/src/index.js | 29 ---- src/client/src/lib/SVGSprite.svelte | 6 + src/client/src/lib/SVGSpriteButton.svelte | 61 +++++++++ src/client/src/lib/app.css | 22 +++ src/client/src/lib/connector.ts | 113 ++++++++++++++++ ...ched_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 + ...mera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg | 1 + ...rcle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg | 1 + ...lect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 + ...load_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg | 1 + ...mage_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 + ..._new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 + ...ause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 + ...lbum_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 + ...rame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 + ...rrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 + src/client/src/lib/icons/removefill.sh | 6 + ...arch_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 + ..._all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg | 1 + ...ings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 + ...hare_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg | 1 + ...next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 + ...ious_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 + ...show_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg | 1 + ..._off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg | 1 + src/client/src/pages.js | 54 -------- src/client/src/routes/+layout.svelte | 98 ++++++++++++++ src/client/src/routes/+page.svelte | 1 + src/client/src/routes/albums/+page.svelte | 77 +++++++++++ src/client/src/routes/settings/+page.svelte | 103 ++++++++++++++ src/client/src/routes/slideshow/+page.svelte | 149 +++++++++++++++++++++ src/client/src/settings.js | 16 --- src/client/src/slides.js | 137 ------------------- src/client/src/style.css | 48 ------- src/client/svelte.config.ts | 21 +++ 60 files changed, 688 insertions(+), 645 deletions(-) delete mode 100644 src/client/index.html delete mode 100644 src/client/src/albums.js create mode 100644 src/client/src/app.html delete mode 100644 src/client/src/connector.js delete mode 100644 src/client/src/icons.js delete mode 100644 src/client/src/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg delete mode 100755 src/client/src/icons/removefill.sh delete mode 100644 src/client/src/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/index.js create mode 100644 src/client/src/lib/SVGSprite.svelte create mode 100644 src/client/src/lib/SVGSpriteButton.svelte create mode 100644 src/client/src/lib/app.css create mode 100644 src/client/src/lib/connector.ts create mode 100644 src/client/src/lib/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100755 src/client/src/lib/icons/removefill.sh create mode 100644 src/client/src/lib/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 src/client/src/lib/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg delete mode 100644 src/client/src/pages.js create mode 100644 src/client/src/routes/+layout.svelte create mode 120000 src/client/src/routes/+page.svelte create mode 100644 src/client/src/routes/albums/+page.svelte create mode 100644 src/client/src/routes/settings/+page.svelte create mode 100644 src/client/src/routes/slideshow/+page.svelte delete mode 100644 src/client/src/settings.js delete mode 100644 src/client/src/slides.js delete mode 100644 src/client/src/style.css create mode 100644 src/client/svelte.config.ts diff --git a/src/client/index.html b/src/client/index.html deleted file mode 100644 index dcecf89..0000000 --- a/src/client/index.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - -
-
- -
-
-
- - -
-
- - -
- - - -
-
- -
-
- - -
-
- -
- -
-
-
-

- - Immich Server -

-
- -

Complete Immich base url (e.g. http://immich.local)

-
- -
- -

Generate an API key in User Settings

-
- -
-
-

- - Display -

-
-
- -

Number of seconds each image will be displayed.

-
- -
- -

Number of seconds each image transition will take.
Set as 0 to disable.

-
- -
- -

Target display framerate.
Simple transitions look good at 12-15 fps.

-
- -
- -

Image size to load on the display.
Large thumbnail size is suitable for FHD.

-
- -
- -

Number of assets that can exist at once in RAM.
Each asset will take ~10x the display size in memory.

-
- -
-
- -
-
-

Immich Pix Frame

-

© Tim Keller 2025

-

GPL-3.0 License

-

View Source

-
-
-
-
- - - diff --git a/src/client/src/albums.js b/src/client/src/albums.js deleted file mode 100644 index 0ac9195..0000000 --- a/src/client/src/albums.js +++ /dev/null @@ -1,87 +0,0 @@ -import apiConnector from "./connector.js" - -class Album { - static albums = [] - static albumTemplate = null - static albumContainer = null - - constructor(data) { - this.data = data - // create clone element - const e = Album.albumTemplate.content.cloneNode(true) - e.firstElementChild.dataset.key = data.id - e.querySelector("a").href = apiConnector.albumSrc(data.id) - e.querySelector("img").src = apiConnector.assetThumbnailSrc(data.albumThumbnailAssetId) - e.querySelector(".album-name").textContent = data.albumName - e.querySelector(".album-assets-count").textContent = data.assetCount.toLocaleString() - if (!data.shared) - e.querySelector(".album-shared").remove() - - Album.albums.push(this) - Album.albumsContainer.appendChild(e) - this.element = Album.albumsContainer.lastElementChild - if (data.selected) - this.element.dataset.selected = "1" - } - - toggleVisibility(visible) { this.element.classList.toggle("hidden!", !visible) } - - static albumSelect(e) { - // find album element - let album = e.target - while (album && !album.classList.contains("album")) - album = album.parentElement - - if (album === null) - return - - if (album.dataset.selected) - delete album.dataset.selected - else - album.dataset.selected = "1" - } - - static albumsFilter(e) { - const q = e.target.value.toLowerCase() - for (const album of Album.albums) { - const match = album.data.albumName.toLowerCase().includes(q) - album.toggleVisibility(match) - } - } - - static getSelected() { - const s = [] - for (const album of Album.albums) - if (album.element.dataset.selected) - s.push(album.data.id) - return s - } - - static submitSelected() { - apiConnector.updateAlbums(Album.getSelected()) - } - - static async initAlbums(albumsPageContainer) { - Album.albumsContainer = albumsPageContainer.querySelector("#albums-container") - Album.albumTemplate = albumsPageContainer.querySelector("#album-template") - const albumSearch = albumsPageContainer.querySelector("#album-search") - const albumsSubmit = albumsPageContainer.querySelector("#albums-submit") - - // create albums - const albumsResponse = await apiConnector.fetchAlbums() - if (!albumsResponse.length) - return false - - for (const res of albumsResponse) - new Album(res) - - // album selection - Album.albumsContainer.addEventListener("click", Album.albumSelect) - albumSearch.addEventListener("input", Album.albumsFilter) - albumsSubmit.addEventListener("click", Album.submitSelected) - - return true - } -} - -export default Album.initAlbums diff --git a/src/client/src/app.html b/src/client/src/app.html new file mode 100644 index 0000000..b1faee9 --- /dev/null +++ b/src/client/src/app.html @@ -0,0 +1,12 @@ + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + diff --git a/src/client/src/connector.js b/src/client/src/connector.js deleted file mode 100644 index 9568dc8..0000000 --- a/src/client/src/connector.js +++ /dev/null @@ -1,92 +0,0 @@ -import io from "socket.io-client" - -class APIConnector { - constructor(url) { - this.url = url ?? "" - this.socket = io(url) - - this.assetIndex = 0 - this.movement = 0 - this.assets = null - this.currentAsset = null - this.seekCallbacks = [] - - this.socket.on("seek", e => { - this.assetIndex = e.asset_index - this.movement = e.movement - this.assets = e.assets - this.currentAsset = e.current_asset - for (const cb of this.seekCallbacks) - cb() - }) - - this.downloadAnchor = document.createElement("a") - this.downloadAnchor.classList = "hidden" - document.body.appendChild(this.downloadAnchor) - } - - fetch(endpoint, c) { - return fetch(this.url + "/api" + endpoint, c ?? {}) - .then(response => { - if (!response.ok) - throw new Error(`HTTP error! Status: ${response.status}`) - return response.json() - }) - .then(data => { - return data - }) - .catch(error => { - console.error("Fetch error:", error) - return null - }) - } - - async assetDownload(key) { - const filename = await this.assetFileName(key) - fetch(this.assetFullsizeSrc(key)) - .then(response => { - if (!response.ok) - throw new Error(`HTTP error! Status: ${response.status}`) - return response.blob() - }) - .then(blob => { - const blobUrl = URL.createObjectURL(blob) - this.downloadAnchor.href = blobUrl - this.downloadAnchor.download = filename - - this.downloadAnchor.click() - URL.revokeObjectURL(blobUrl) - }) - .catch(error => { - console.error("Fetch error:", error) - return null - }) - } - - post(endpoint, body) { - return this.fetch(endpoint, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }) - } - - seek(increment) { - this.socket.emit("seek", increment) - } - - fetchAlbums() { return this.fetch("/albums") } - fetchConfig() { return this.fetch("/config") } - updateAlbums(albums) { return this.post("/albums/update", albums) } - updateConfig(config) { return this.post("/config/update", config) } - - albumSrc(key) { return `${this.url}/api/redirect/albums/${key}` } - - assetPreviewSrc(key) { return `${this.url}/api/asset/${key}` } - assetThumbnailSrc(key) { return `${this.url}/api/asset/${key}/thumbnail` } - assetFullsizeSrc(key) { return `${this.url}/api/asset/${key}/fullsize` } - assetFileName(key) { return this.fetch(`/asset/${key}/filename`).then(d => d.filename) } -} - -const apiConnector = new APIConnector(window.location.origin) -export default apiConnector diff --git a/src/client/src/icons.js b/src/client/src/icons.js deleted file mode 100644 index 529d509..0000000 --- a/src/client/src/icons.js +++ /dev/null @@ -1,19 +0,0 @@ -import "./icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg" -import "./icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg" -import "./icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg" -import "./icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg" -import "./icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg" -import "./icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg" -import "./icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg" -import "./icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg" -import "./icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg" diff --git a/src/client/src/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 5e389dc..0000000 --- a/src/client/src/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 06d30f0..0000000 --- a/src/client/src/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 028526b..0000000 --- a/src/client/src/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg deleted file mode 100644 index dd46e14..0000000 --- a/src/client/src/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg deleted file mode 100644 index e9ef3c9..0000000 --- a/src/client/src/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 1ff79ee..0000000 --- a/src/client/src/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 12e0802..0000000 --- a/src/client/src/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index aa7aece..0000000 --- a/src/client/src/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 95ead34..0000000 --- a/src/client/src/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index e26dccc..0000000 --- a/src/client/src/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 22dd0af..0000000 --- a/src/client/src/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/removefill.sh b/src/client/src/icons/removefill.sh deleted file mode 100755 index 399e8e4..0000000 --- a/src/client/src/icons/removefill.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -# Google Material Icons -- complete default settings -sed -Ei 's/ ?fill="#[0-9a-fA-F]{6}"//' *.svg -for svg in *.svg; do - echo "import \"./icons/$svg\"" -done diff --git a/src/client/src/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 1d95298..0000000 --- a/src/client/src/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 3a6618e..0000000 --- a/src/client/src/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 92240e4..0000000 --- a/src/client/src/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 911d24e..0000000 --- a/src/client/src/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index ba0282f..0000000 --- a/src/client/src/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index ea45ca0..0000000 --- a/src/client/src/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 17fb6dd..0000000 --- a/src/client/src/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg deleted file mode 100644 index 41b5681..0000000 --- a/src/client/src/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/client/src/index.js b/src/client/src/index.js deleted file mode 100644 index 7d7c9db..0000000 --- a/src/client/src/index.js +++ /dev/null @@ -1,29 +0,0 @@ -import "@fontsource/overpass" -import "@fontsource/overpass/700.css" -import "@fontsource/overpass/500.css" -import "./style.css" -import Page from "./pages.js" -import "./icons.js" -import initSlides from "./slides.js" -import initAlbums from "./albums.js" -import initSettings from "./settings.js" - -const slideshow = new Page(document.querySelector("#slideshow"), ["", "/slideshow"], initSlides) -const albums = new Page(document.querySelector("#albums"), ["/albums"], initAlbums) -const settings = new Page(document.querySelector("#settings"), ["/settings"], initSettings) - -window.addEventListener("popstate", Page.pathnameCallback) -Page.pathnameCallback() /* run after all pages are registered */ - -/* add event listeners for anchor elements in footer */ -function softRedirect(e) { - e.preventDefault() - let a = e.target - if (a === null) return - while (a !== null && a.tagName !== "A") - a = a.parentElement - if (a === null) return - Page.softRedirect(a.href) -} - -document.querySelector("#menu").addEventListener("click", softRedirect) diff --git a/src/client/src/lib/SVGSprite.svelte b/src/client/src/lib/SVGSprite.svelte new file mode 100644 index 0000000..ff965c7 --- /dev/null +++ b/src/client/src/lib/SVGSprite.svelte @@ -0,0 +1,6 @@ + + + diff --git a/src/client/src/lib/SVGSpriteButton.svelte b/src/client/src/lib/SVGSpriteButton.svelte new file mode 100644 index 0000000..a9c9fcb --- /dev/null +++ b/src/client/src/lib/SVGSpriteButton.svelte @@ -0,0 +1,61 @@ + + + + +{#snippet inner()} + + + {#if label} + {label} + {/if} +{/snippet} + +{#if href} + + {@render inner()} + +{:else} + +{/if} diff --git a/src/client/src/lib/app.css b/src/client/src/lib/app.css new file mode 100644 index 0000000..b88882b --- /dev/null +++ b/src/client/src/lib/app.css @@ -0,0 +1,22 @@ +@import "tailwindcss"; + +:root { + --font-sans: "Overpass", sans-serif; + @apply scheme-dark +} + +.rounded-fieldset { + @apply border border-gray-500 rounded-2xl p-6 flex flex-col gap-4 +} +.fieldset-header { + @apply font-bold text-blue-300 fill-blue-300 text-xl +} +.settings-label { + @apply font-medium text-blue-200 +} +.rounded-input { + @apply rounded-2xl bg-zinc-800 p-4 +} +.rounded-btn { + @apply rounded-full w-fit bg-blue-300 px-4 py-2 text-black fill-black font-medium cursor-pointer flex gap-1 +} diff --git a/src/client/src/lib/connector.ts b/src/client/src/lib/connector.ts new file mode 100644 index 0000000..0224438 --- /dev/null +++ b/src/client/src/lib/connector.ts @@ -0,0 +1,113 @@ +import io from "socket.io-client" + +export interface Album { + id: string + albumThumbnailAssetId: string + albumName: string + assetCount: number + shared: boolean + selected: boolean +} + +export interface Config { + immich_url: string + immich_api_key: string + image_duration: number + transition_duration: number + max_framerate: number + auto_transition: boolean + display_size: "fullsize" | "preview" | "thumbnail" + max_cache_assets: number + album_list?: string[] +} + +class APIConnector { + constructor(url) { + this.url = url ?? "" + this.socket = io(url) + + this.assetIndex = 0 + this.movement = 0 + this.assets = null + this.currentAsset = null + this.seekCallbacks = [] + + this.socket.on("seek", e => { + this.assetIndex = e.asset_index + this.movement = e.movement + this.assets = e.assets + this.currentAsset = e.current_asset + for (const cb of this.seekCallbacks) + cb() + }) + + this.downloadAnchor = document.createElement("a") + this.downloadAnchor.classList = "hidden" + document.body.appendChild(this.downloadAnchor) + } + + fetch(endpoint, c) { + return fetch(this.url + "/api" + endpoint, c ?? {}) + .then(response => { + if (!response.ok) + throw new Error(`HTTP error! Status: ${response.status}`) + return response.json() + }) + .then(data => { + return data + }) + .catch(error => { + console.error("Fetch error:", error) + return null + }) + } + + async assetDownload(key) { + const filename = await this.assetFileName(key) + fetch(this.assetFullsizeSrc(key)) + .then(response => { + if (!response.ok) + throw new Error(`HTTP error! Status: ${response.status}`) + return response.blob() + }) + .then(blob => { + const blobUrl = URL.createObjectURL(blob) + this.downloadAnchor.href = blobUrl + this.downloadAnchor.download = filename + + this.downloadAnchor.click() + URL.revokeObjectURL(blobUrl) + }) + .catch(error => { + console.error("Fetch error:", error) + return null + }) + } + + post(endpoint, body) { + return this.fetch(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }) + } + + seek(increment) { + this.socket.emit("seek", increment) + } + + fetchAlbums() { return this.fetch("/albums") } + fetchConfig() { return this.fetch("/config") } + updateAlbums(albums) { return this.post("/albums/update", albums) } + updateConfig(config) { return this.post("/config/update", config) } + + albumSrc(key) { return `${this.url}/api/redirect/albums/${key}` } + + assetPreviewSrc(key) { return `${this.url}/api/asset/${key}` } + assetThumbnailSrc(key) { return `${this.url}/api/asset/${key}/thumbnail` } + assetFullsizeSrc(key) { return `${this.url}/api/asset/${key}/fullsize` } + assetFileName(key) { return this.fetch(`/asset/${key}/filename`).then(d => d.filename) } +} + +const apiConnector = new APIConnector("http://192.168.1.182:8080/") +export default apiConnector diff --git a/src/client/src/lib/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..5e389dc --- /dev/null +++ b/src/client/src/lib/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..06d30f0 --- /dev/null +++ b/src/client/src/lib/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..028526b --- /dev/null +++ b/src/client/src/lib/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..dd46e14 --- /dev/null +++ b/src/client/src/lib/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..e9ef3c9 --- /dev/null +++ b/src/client/src/lib/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..1ff79ee --- /dev/null +++ b/src/client/src/lib/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..12e0802 --- /dev/null +++ b/src/client/src/lib/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..aa7aece --- /dev/null +++ b/src/client/src/lib/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..95ead34 --- /dev/null +++ b/src/client/src/lib/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..e26dccc --- /dev/null +++ b/src/client/src/lib/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..22dd0af --- /dev/null +++ b/src/client/src/lib/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/removefill.sh b/src/client/src/lib/icons/removefill.sh new file mode 100755 index 0000000..399e8e4 --- /dev/null +++ b/src/client/src/lib/icons/removefill.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Google Material Icons -- complete default settings +sed -Ei 's/ ?fill="#[0-9a-fA-F]{6}"//' *.svg +for svg in *.svg; do + echo "import \"./icons/$svg\"" +done diff --git a/src/client/src/lib/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..1d95298 --- /dev/null +++ b/src/client/src/lib/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..3a6618e --- /dev/null +++ b/src/client/src/lib/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..92240e4 --- /dev/null +++ b/src/client/src/lib/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..911d24e --- /dev/null +++ b/src/client/src/lib/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..ba0282f --- /dev/null +++ b/src/client/src/lib/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..ea45ca0 --- /dev/null +++ b/src/client/src/lib/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..17fb6dd --- /dev/null +++ b/src/client/src/lib/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/lib/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/src/client/src/lib/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg new file mode 100644 index 0000000..41b5681 --- /dev/null +++ b/src/client/src/lib/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/src/pages.js b/src/client/src/pages.js deleted file mode 100644 index e2199a4..0000000 --- a/src/client/src/pages.js +++ /dev/null @@ -1,54 +0,0 @@ -const menu = document.querySelector("#menu") - -export default class Page { - static pages = {} - static currentPage = null - - static pathnameCallback() { - const path = window.location.pathname.replace(/\/$/, "") - const page = Page.pages[path] - - if (!page) - throw new Error(`Path '${path}' does not exist`) - - if (Page.currentPage) - Page.currentPage.setVisible(false) - - page.setVisible(true) - Page.currentPage = page - } - - static softRedirect(path) { - window.history.pushState({}, "", path) - Page.pathnameCallback() - } - - constructor(pageContainer, endpoints, f_initialize) { - for (const endpoint of endpoints) - Page.pages[endpoint] = this - - this.pageContainer = pageContainer - this.endpoints = endpoints - this.initialize = f_initialize - this.visible = false - this.initialized = false - } - - async setVisible(visible) { - this.pageContainer.classList.toggle("hidden!", !visible) - this.visible = visible - if (this.visible) { - /* initialize page */ - if (!this.initialized && this.initialize) - this.initialized = await this.initialize(this.pageContainer) - - /* set selected attribute on the link */ - for (const a of menu.querySelectorAll("a")) { - if (this.endpoints.includes(a.getAttribute("href"))) - a.dataset.selected = "1" - else - delete a.dataset.selected - } - } - } -} diff --git a/src/client/src/routes/+layout.svelte b/src/client/src/routes/+layout.svelte new file mode 100644 index 0000000..d963f02 --- /dev/null +++ b/src/client/src/routes/+layout.svelte @@ -0,0 +1,98 @@ + + + + + + + + +
+
+
+ {@render children?.()} +
+ +
+
+ +
+
+
diff --git a/src/client/src/routes/+page.svelte b/src/client/src/routes/+page.svelte new file mode 120000 index 0000000..ff71e47 --- /dev/null +++ b/src/client/src/routes/+page.svelte @@ -0,0 +1 @@ +slideshow/+page.svelte \ No newline at end of file diff --git a/src/client/src/routes/albums/+page.svelte b/src/client/src/routes/albums/+page.svelte new file mode 100644 index 0000000..9534307 --- /dev/null +++ b/src/client/src/routes/albums/+page.svelte @@ -0,0 +1,77 @@ + + +
+
+ +
+ + +
+ +
+ {#each albums as album} + + {/each} +
+
+
diff --git a/src/client/src/routes/settings/+page.svelte b/src/client/src/routes/settings/+page.svelte new file mode 100644 index 0000000..a0483a0 --- /dev/null +++ b/src/client/src/routes/settings/+page.svelte @@ -0,0 +1,103 @@ + + +
+
+
+

+ + Immich Server +

+
+ +

Complete Immich base url (e.g. http://immich.local)

+
+ +
+ +

Generate an API key in User Settings

+
+ +
+
+

+ + Display +

+
+
+ +

Number of seconds each image will be displayed.

+
+ +
+ +

Number of seconds each image transition will take.
Set as 0 to disable.

+
+ +
+ +

Target display framerate.
Simple transitions look good at 12-15 fps.

+
+ +
+ +

Image size to load on the display.
Large thumbnail size is suitable for FHD.

+
+ +
+ +

Number of assets that can exist at once in RAM.
Each asset will take ~10x the display size in memory.

+
+ +
+
+ +
+
+

Immich Pix Frame

+

© Tim Keller 2025

+

GPL-3.0 License

+

View Source

+
+
+
diff --git a/src/client/src/routes/slideshow/+page.svelte b/src/client/src/routes/slideshow/+page.svelte new file mode 100644 index 0000000..df2d352 --- /dev/null +++ b/src/client/src/routes/slideshow/+page.svelte @@ -0,0 +1,149 @@ + + +
+ +
+
+ + +
+
+ +
+ {#each assetSrcs as src} +
+ +
+ {/each} +
+ +
+ { flickity.previous(); seek() }}> + {#if false} + {}}> + {:else} + {}}> + {/if} + { flickity.next(); seek() }}> +
+
diff --git a/src/client/src/settings.js b/src/client/src/settings.js deleted file mode 100644 index fd8bdad..0000000 --- a/src/client/src/settings.js +++ /dev/null @@ -1,16 +0,0 @@ -import apiConnector from "./connector.js" - -export default async function initSettings(settingsPageContainer) { - const inputList = Array.from(settingsPageContainer.querySelectorAll("[name]")) - const inputs = Object.fromEntries(inputList.map(e => [e.name, e])) - const currentConfig = await apiConnector.fetchConfig() - - for (const [name, value] of Object.entries(currentConfig)) - if (inputs[name]) - inputs[name].value = value - - settingsPageContainer.querySelector("#settings-submit").addEventListener("click", e => { - e.preventDefault() - apiConnector.updateConfig(Object.fromEntries(inputList.map(el => [el.name, el.type === "number" ? parseFloat(el.value) : el.value]))) - }) -} diff --git a/src/client/src/slides.js b/src/client/src/slides.js deleted file mode 100644 index a0d58ac..0000000 --- a/src/client/src/slides.js +++ /dev/null @@ -1,137 +0,0 @@ -import Flickity from "flickity" -import "flickity/dist/flickity.min.css" -import apiConnector from "./connector.js" - -class Slides { - constructor(slidesContainer) { - /* previous selected index */ - this.selectedIndex = 0 - this.assetIndex = 0 - - this.slidesContainer = slidesContainer - - /* append 11 cells to carousel */ - const carousel = this.slidesContainer.querySelector("#slideshow-carousel") - const cellTemplate = this.slidesContainer.querySelector("#carousel-cell-template") - this.cells = [] - for (let i = 0; i < 11; i++) - carousel.appendChild(cellTemplate.content.cloneNode(true)) - - /* initialize slides */ - this.flickity = new Flickity(carousel, { - wrapAround: true, - prevNextButtons: false, - pageDots: false, - resize: true, - setGallerySize: false, - }) - - this.flickity.on("scroll", progress => { this.scroll(progress) }) - this.flickity.on("staticClick", (e, pointer, cellElement, cellIndex) => { this.staticClick(e, pointer, cellElement, cellIndex) }) - this.flickity.on("dragEnd", () => { this.seek() }) - this.initImages() - - /* initialize seek buttons */ - const seekPrevButton = this.slidesContainer.querySelector("#prev-slide") - const seekNextButton = this.slidesContainer.querySelector("#next-slide") - - seekPrevButton.addEventListener("click", () => { this.flickity.previous() ; this.seek() }) - seekNextButton.addEventListener("click", () => { this.flickity.next() ; this.seek() }) - - /* initialize seek callback */ - apiConnector.seekCallbacks.push(c => { this.seekCallback() }) - - /* initialize top controls */ - const assetDownloadButton = this.slidesContainer.querySelector("#download") - - assetDownloadButton.addEventListener("click", () => { apiConnector.assetDownload(apiConnector.currentAsset) }) - } - - seek() { - // this is just like calculating movement in lazycachelist.py - // gets the min of the absolute values and returns signed value - const increment = [ - this.flickity.selectedIndex - this.selectedIndex, // no list wrap - this.flickity.selectedIndex - this.flickity.cells.length, // wrap backwards (0 -> -1) - this.flickity.cells.length - this.selectedIndex, // wrap forwards (-1 -> 0) - ].reduce((key, v) => Math.abs(v) < Math.abs(key) ? v : key) - this.selectedIndex = this.flickity.selectedIndex - this.assetIndex += increment - apiConnector.seek(increment) - } - - seekCallback() { - let i - if (this.assetIndex !== apiConnector.assetIndex) { - this.assetIndex = apiConnector.assetIndex - i = apiConnector.movement - - for (; i > 0; i--) this.flickity.next() - for (; i < 0; i++) this.flickity.previous() - this.selectedIndex = this.flickity.selectedIndex - } - - // load new imgs - // TODO need to make the 11 cells a constant somehow - for (i = 0; i < this.flickity.cells.length; i++) { - const x = (i + this.selectedIndex + 6) % this.flickity.cells.length - const e = this.flickity.cells[x].element - const img = e.firstElementChild - img.src = apiConnector.assetPreviewSrc(apiConnector.assets[i]) - } - } - - /* Flickity function for scrolling to ensure next and prev pics are always - * visible and to transition between states */ - scroll(progress) { - const normalizedProgress = progress / (1 / (this.flickity.cells.length-1)) - const liveSelectedIndex = Math.round(normalizedProgress) - const localizedProgress = normalizedProgress - liveSelectedIndex - - const prevSelectedCell = this.flickity.cells.at((liveSelectedIndex-1) % this.flickity.cells.length).element - const liveSelectedCell = this.flickity.cells.at((liveSelectedIndex ) % this.flickity.cells.length).element - const nextSelectedCell = this.flickity.cells.at((liveSelectedIndex+1) % this.flickity.cells.length).element - - const prevSelectedImage = prevSelectedCell.firstElementChild - const liveSelectedImage = liveSelectedCell.firstElementChild - const nextSelectedImage = nextSelectedCell.firstElementChild - - const prevMargin = prevSelectedCell.clientWidth - prevSelectedImage.clientWidth - const liveMargin = liveSelectedCell.clientWidth - liveSelectedImage.clientWidth - const nextMargin = nextSelectedCell.clientWidth - nextSelectedImage.clientWidth - - liveSelectedImage.style.marginLeft = liveMargin / 2 + "px" - nextSelectedImage.style.marginLeft = Math.max(0, Math.min(nextMargin, nextMargin * (localizedProgress))) + "px" - prevSelectedImage.style.marginLeft = prevMargin - Math.max(0, Math.min(prevMargin, prevMargin * localizedProgress * -1)) + "px" // TODO clean this - } - - /* jump to clicked on slide */ - staticClick(e, pointer, cellElement, cellIndex) { - this.flickity.select(cellIndex) - this.seek() - } - - /* make sure images have correct margin when loaded since scroll function - * depends on them being loaded */ - positionImageStatic(img) { - const i = parseInt(img.dataset.index) - if (i == this.flickity.selectedIndex) - img.style.marginLeft = (img.parentElement.clientWidth - img.clientWidth) / 2 + "px" - else if ((i + 1) % this.flickity.cells.length == this.flickity.selectedIndex) - img.style.marginLeft = img.parentElement.clientWidth - img.clientWidth + "px" - } - - imageLoaded(e) { this.positionImageStatic(e.target) } - initImages() { - const imgs = this.slidesContainer.querySelectorAll("#slideshow-carousel img") - for (let i = 0; i < imgs.length; i++) { - const img = imgs[i] - img.dataset.index = i - img.addEventListener("load", e => { this.imageLoaded(e) }) - if (img.complete) - this.positionImageStatic(img) - } - } -} - -export default slidesContainer => { new Slides(slidesContainer); return true } diff --git a/src/client/src/style.css b/src/client/src/style.css deleted file mode 100644 index 552b080..0000000 --- a/src/client/src/style.css +++ /dev/null @@ -1,48 +0,0 @@ -@import "tailwindcss"; -@source "../index.html"; - -:root { - --font-sans: "Overpass", sans-serif; - @apply scheme-dark -} - -.svg-btn { - @apply bg-none border-0 p-0 cursor-pointer -} - -.nav-btn { - @apply flex items-center justify-center no-underline w-full gap-4 p-3 rounded-full - hover:text-blue-300 hover:fill-blue-300 - data-selected:text-blue-300 data-selected:fill-blue-300 - data-selected:bg-gray-900 -} - -.carousel-cell { - @apply h-full w-[70vw] mx-3 md:w-[80vw] md:mx-6 -} -.carousel-img { - @apply align-middle h-full max-w-full object-contain -} - -.album { - @apply flex relative h-40 gap-6 p-3 cursor-pointer - border-y border-slate-800 - data-selected:bg-slate-950 data-selected:hover:bg-slate-900 - hover:bg-gray-900 -} - -.rounded-fieldset { - @apply border border-gray-500 rounded-2xl p-6 flex flex-col gap-4 -} -.fieldset-header { - @apply font-bold text-blue-300 fill-blue-300 text-xl -} -.settings-label { - @apply font-medium text-blue-200 -} -.rounded-input { - @apply rounded-2xl bg-zinc-800 p-4 -} -.rounded-btn { - @apply rounded-full w-fit bg-blue-300 px-4 py-2 text-black fill-black font-medium cursor-pointer flex gap-1 -} diff --git a/src/client/svelte.config.ts b/src/client/svelte.config.ts new file mode 100644 index 0000000..e6eb2e2 --- /dev/null +++ b/src/client/svelte.config.ts @@ -0,0 +1,21 @@ +import adapter from "@sveltejs/adapter-static" + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + output: { + bundleStrategy: "inline", + }, + router: { + type: "hash", + }, + adapter: adapter({ + // default options are shown. On some platforms + // these options are set automatically — see below + pages: "../../dist", + fallback: "index.html", + }) + } +} + +export default config -- cgit v1.2.3