From ffa5ff333eabffe07394fb21bc413d7d238ee651 Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Tue, 24 Jun 2025 19:23:33 -0500 Subject: move all files to /static --- static/src/albums.js | 85 +++++++++++++ static/src/connector.js | 91 ++++++++++++++ static/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 + static/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 + static/src/index.js | 29 +++++ static/src/pages.js | 54 ++++++++ static/src/settings.js | 16 +++ static/src/slides.js | 137 +++++++++++++++++++++ static/src/style.css | 48 ++++++++ 28 files changed, 504 insertions(+) create mode 100644 static/src/albums.js create mode 100644 static/src/connector.js create mode 100644 static/src/icons.js create mode 100644 static/src/icons/cached_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/camera_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/check_circle_24dp_E3E3E3_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/deselect_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/download_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/image_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/open_in_new_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/pause_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/photo_album_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/photo_frame_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/play_arrow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100755 static/src/icons/removefill.sh create mode 100644 static/src/icons/search_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/select_all_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/settings_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/share_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/skip_next_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/skip_previous_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/slideshow_24dp_FFFFFF_FILL1_wght400_GRAD0_opsz24.svg create mode 100644 static/src/icons/visibility_off_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg create mode 100644 static/src/index.js create mode 100644 static/src/pages.js create mode 100644 static/src/settings.js create mode 100644 static/src/slides.js create mode 100644 static/src/style.css (limited to 'static/src') 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 +} -- cgit v1.2.3