summaryrefslogtreecommitdiff
path: root/static/src
diff options
context:
space:
mode:
authorTim Keller <tjk@tjkeller.xyz>2025-06-24 19:23:33 -0500
committerTim Keller <tjk@tjkeller.xyz>2025-06-24 19:23:33 -0500
commitffa5ff333eabffe07394fb21bc413d7d238ee651 (patch)
tree007b506aca746fda9040ea4f0ffa8d6578e86f7e /static/src
parent09e9193b6be6e2eae7ffbfbfedd15ac22bab0022 (diff)
downloadimmich-frame-ffa5ff333eabffe07394fb21bc413d7d238ee651.tar.xz
immich-frame-ffa5ff333eabffe07394fb21bc413d7d238ee651.zip
move all files to /static
Diffstat (limited to 'static/src')
-rw-r--r--static/src/albums.js85
-rw-r--r--static/src/connector.js91
-rw-r--r--static/src/icons.js19
-rw-r--r--static/src/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg1
-rwxr-xr-xstatic/src/icons/removefill.sh6
-rw-r--r--static/src/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg1
-rw-r--r--static/src/index.js29
-rw-r--r--static/src/pages.js54
-rw-r--r--static/src/settings.js16
-rw-r--r--static/src/slides.js137
-rw-r--r--static/src/style.css48
28 files changed, 504 insertions, 0 deletions
diff --git a/static/src/albums.js b/static/src/albums.js
new file mode 100644
index 0000000..b7523d9
--- /dev/null
+++ b/static/src/albums.js
@@ -0,0 +1,85 @@
+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()
+
+ 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/static/src/connector.js b/static/src/connector.js
new file mode 100644
index 0000000..5d8b62f
--- /dev/null
+++ b/static/src/connector.js
@@ -0,0 +1,91 @@
+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)
+ })
+ }
+
+ async assetDownload(key) {
+ const filename = await this.assetFileName(key)
+ console.log(filename)
+ 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)
+ })
+ }
+
+ 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://localhost:5000")
+export default apiConnector
diff --git a/static/src/icons.js b/static/src/icons.js
new file mode 100644
index 0000000..529d509
--- /dev/null
+++ b/static/src/icons.js
@@ -0,0 +1,19 @@
+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/static/src/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/static/src/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..5e389dc
--- /dev/null
+++ b/static/src/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M482-160q-134 0-228-93t-94-227v-7l-64 64-56-56 160-160 160 160-56 56-64-64v7q0 100 70.5 170T482-240q26 0 51-6t49-18l60 60q-38 22-78 33t-82 11Zm278-161L600-481l56-56 64 64v-7q0-100-70.5-170T478-720q-26 0-51 6t-49 18l-60-60q38-22 78-33t82-11q134 0 228 93t94 227v7l64-64 56 56-160 160Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..06d30f0
--- /dev/null
+++ b/static/src/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M500-600q-11 0-17-10t0-20l117-203q8-13 20-15.5t26 4.5q69 31 122 86.5T850-632q5 13-1.5 22.5T828-600H500Zm-143 49L237-759q-8-13-4-26t16-21q48-34 106.5-54T480-880q15 0 33 1.5t30 3.5q14 2 18.5 12t-2.5 22L392-551q-6 10-17.5 10T357-551ZM114-400q-11 0-20-7.5T83-426q-2-11-2.5-24t-.5-30q0-63 20.5-125.5T164-724q11-14 25.5-14t22.5 15l168 293q6 10-.5 20T362-400H114Zm200 284q-66-32-121-87t-83-125q-5-13 2-22.5t21-9.5h326q11 0 17 10t0 20L361-129q-8 13-20 17t-27-4Zm166 36q-14 0-31.5-1.5T418-85q-14-2-19-12t2-22l165-288q6-10 20-10t20 10l118 205q7 11 4.5 24T712-154q-46 34-107.5 54T480-80Zm271-155L581-530q-6-10 .5-20t17.5-10h247q11 0 20 7.5t11 18.5q2 11 2.5 24t.5 30q0 63-20.5 126.5T796-235q-8 11-23 11.5T751-235Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..028526b
--- /dev/null
+++ b/static/src/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="m424-296 282-282-56-56-226 226-114-114-56 56 170 170Zm56 216q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/static/src/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..dd46e14
--- /dev/null
+++ b/static/src/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M791-56 567-280H280v-287L56-791l56-57 736 736-57 56ZM360-360h127L360-487v127Zm320-33-80-80v-127H473l-80-80h287v287ZM200-200v80q-33 0-56.5-23.5T120-200h80Zm-80-80v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm160 480v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 480v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80q33 0 56.5 23.5T840-760h-80Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/static/src/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..e9ef3c9
--- /dev/null
+++ b/static/src/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..1ff79ee
--- /dev/null
+++ b/static/src/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm40-160h480L570-480 450-320l-90-120-120 160Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/static/src/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..12e0802
--- /dev/null
+++ b/static/src/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..aa7aece
--- /dev/null
+++ b/static/src/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M560-200v-560h160v560H560Zm-320 0v-560h160v560H240Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..95ead34
--- /dev/null
+++ b/static/src/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h480q33 0 56.5 23.5T800-800v640q0 33-23.5 56.5T720-80H240Zm40-160h400L545-420 440-280l-65-87-95 127Zm160-280 100-60 100 60v-280H440v280Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..e26dccc
--- /dev/null
+++ b/static/src/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M240-120q-17 0-28.5-11.5T200-160v-40h-80q-33 0-56.5-23.5T40-280v-440q0-33 23.5-56.5T120-800h720q33 0 56.5 23.5T920-720v440q0 33-23.5 56.5T840-200h-80v40q0 17-11.5 28.5T720-120H240Zm-40-240h560L580-600 440-420 340-540 200-360Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..22dd0af
--- /dev/null
+++ b/static/src/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M320-200v-560l440 280-440 280Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/removefill.sh b/static/src/icons/removefill.sh
new file mode 100755
index 0000000..399e8e4
--- /dev/null
+++ b/static/src/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/static/src/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/static/src/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..1d95298
--- /dev/null
+++ b/static/src/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg b/static/src/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..3a6618e
--- /dev/null
+++ b/static/src/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M280-280v-400h400v400H280Zm80-80h240v-240H360v240ZM200-200v80q-33 0-56.5-23.5T120-200h80Zm-80-80v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm80-160h-80q0-33 23.5-56.5T200-840v80Zm80 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80q0 33-23.5 56.5T760-120Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80q33 0 56.5 23.5T840-760h-80Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..92240e4
--- /dev/null
+++ b/static/src/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm112-260q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/static/src/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..911d24e
--- /dev/null
+++ b/static/src/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M680-80q-50 0-85-35t-35-85q0-6 3-28L282-392q-16 15-37 23.5t-45 8.5q-50 0-85-35t-35-85q0-50 35-85t85-35q24 0 45 8.5t37 23.5l281-164q-2-7-2.5-13.5T560-760q0-50 35-85t85-35q50 0 85 35t35 85q0 50-35 85t-85 35q-24 0-45-8.5T598-672L317-508q2 7 2.5 13.5t.5 14.5q0 8-.5 14.5T317-452l281 164q16-15 37-23.5t45-8.5q50 0 85 35t35 85q0 50-35 85t-85 35Zm0-80q17 0 28.5-11.5T720-200q0-17-11.5-28.5T680-240q-17 0-28.5 11.5T640-200q0 17 11.5 28.5T680-160ZM200-440q17 0 28.5-11.5T240-480q0-17-11.5-28.5T200-520q-17 0-28.5 11.5T160-480q0 17 11.5 28.5T200-440Zm480-280q17 0 28.5-11.5T720-760q0-17-11.5-28.5T680-800q-17 0-28.5 11.5T640-760q0 17 11.5 28.5T680-720Zm0 520ZM200-480Zm480-280Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..ba0282f
--- /dev/null
+++ b/static/src/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M660-240v-480h80v480h-80Zm-440 0v-480l360 240-360 240Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..ea45ca0
--- /dev/null
+++ b/static/src/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M220-240v-480h80v480h-80Zm520 0L380-480l360-240v480Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg b/static/src/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..17fb6dd
--- /dev/null
+++ b/static/src/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="m380-300 280-180-280-180v360ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Z"/></svg> \ No newline at end of file
diff --git a/static/src/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg b/static/src/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg
new file mode 100644
index 0000000..41b5681
--- /dev/null
+++ b/static/src/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z"/></svg> \ No newline at end of file
diff --git a/static/src/index.js b/static/src/index.js
new file mode 100644
index 0000000..7d7c9db
--- /dev/null
+++ b/static/src/index.js
@@ -0,0 +1,29 @@
+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/static/src/pages.js b/static/src/pages.js
new file mode 100644
index 0000000..f6a8b9b
--- /dev/null
+++ b/static/src/pages.js
@@ -0,0 +1,54 @@
+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
+ }
+
+ setVisible(visible) {
+ this.pageContainer.classList.toggle("hidden!", !visible)
+ this.visible = visible
+ if (this.visible) {
+ /* initialize page */
+ if (!this.initialized && this.initialize)
+ this.initialized = 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/static/src/settings.js b/static/src/settings.js
new file mode 100644
index 0000000..fd8bdad
--- /dev/null
+++ b/static/src/settings.js
@@ -0,0 +1,16 @@
+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/static/src/slides.js b/static/src/slides.js
new file mode 100644
index 0000000..e4f6406
--- /dev/null
+++ b/static/src/slides.js
@@ -0,0 +1,137 @@
+const Flickity = require("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/static/src/style.css b/static/src/style.css
new file mode 100644
index 0000000..b762eb3
--- /dev/null
+++ b/static/src/style.css
@@ -0,0 +1,48 @@
+@import "tailwindcss" source(none);
+@source "../public/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
+}