diff options
| author | Tim Keller <tjk@tjkeller.xyz> | 2025-06-20 22:31:41 -0500 |
|---|---|---|
| committer | Tim Keller <tjk@tjkeller.xyz> | 2025-06-20 22:31:41 -0500 |
| commit | 1ce57c114cf16337748bf5b6e0a0e125b87d7869 (patch) | |
| tree | 55f823cfca8841ca136c8d9585493b3fa5b85cce /src | |
| parent | fb0ef21fc43fbdc687b068ccd423d69173b8e1be (diff) | |
| download | immich-frame-1ce57c114cf16337748bf5b6e0a0e125b87d7869.tar.xz immich-frame-1ce57c114cf16337748bf5b6e0a0e125b87d7869.zip | |
asset download and album search + set
Diffstat (limited to 'src')
| -rw-r--r-- | src/albums.js | 89 | ||||
| -rw-r--r-- | src/connector.js | 38 | ||||
| -rw-r--r-- | src/slides.js | 11 | ||||
| -rw-r--r-- | src/style.css | 2 |
4 files changed, 108 insertions, 32 deletions
diff --git a/src/albums.js b/src/albums.js index c1d85e3..ff7cd1d 100644 --- a/src/albums.js +++ b/src/albums.js @@ -1,30 +1,30 @@ import apiConnector from "./connector.js" -export default async function initAlbums(albumsPageContainer) { - const albumsContainer = albumsPageContainer.querySelector("#albums-container") - const albumTemplate = albumsPageContainer.querySelector("#album-template") - - // create albums - async function createAlbum(res) { - const albumClone = albumTemplate.content.cloneNode(true) - albumClone.firstElementChild.dataset.key = res.id - albumClone.querySelector("a").href = apiConnector.albumSrc(res.id) - albumClone.querySelector("img").src = apiConnector.assetThumbnailSrc(res.albumThumbnailAssetId) - albumClone.querySelector(".album-name").textContent = res.albumName - albumClone.querySelector(".album-assets-count").textContent = res.assetCount.toLocaleString() - if (!res.shared) - albumClone.querySelector(".album-shared").remove() - - albumsContainer.appendChild(albumClone) - } +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() - const albumsResponse = await apiConnector.fetchAlbums() + Album.albums.push(this) + Album.albumsContainer.appendChild(e) + this.element = Album.albumsContainer.lastElementChild + } - for (const res of albumsResponse) - createAlbum(res) + toggleVisibility(visible) { this.element.classList.toggle("hidden!", !visible) } - // album selection - albumsContainer.addEventListener("click", e => { + static albumSelect(e) { // find album element let album = e.target while (album && !album.classList.contains("album")) @@ -33,12 +33,51 @@ export default async function initAlbums(albumsPageContainer) { if (album === null) return - console.log(album) 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 + } - return true + 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/src/connector.js b/src/connector.js index 29f0cff..5d8b62f 100644 --- a/src/connector.js +++ b/src/connector.js @@ -5,18 +5,24 @@ class APIConnector { this.url = url ?? "" this.socket = io(url) - this.asset_index = 0 + 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) { @@ -34,6 +40,28 @@ class APIConnector { }) } + 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", @@ -48,11 +76,15 @@ class APIConnector { 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}` } - assetSrc(key) { return `${this.url}/api/asset/${key}` } + 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") diff --git a/src/slides.js b/src/slides.js index fad6472..e4f6406 100644 --- a/src/slides.js +++ b/src/slides.js @@ -32,14 +32,19 @@ class Slides { this.initImages() /* initialize seek buttons */ - const seekPrevButton = this.slidesContainer.querySelector("#prevSlide") - const seekNextButton = this.slidesContainer.querySelector("#nextSlide") + 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() { @@ -72,7 +77,7 @@ class Slides { const x = (i + this.selectedIndex + 6) % this.flickity.cells.length const e = this.flickity.cells[x].element const img = e.firstElementChild - img.src = apiConnector.assetSrc(apiConnector.assets[i]) + img.src = apiConnector.assetPreviewSrc(apiConnector.assets[i]) } } diff --git a/src/style.css b/src/style.css index a41fa4c..b762eb3 100644 --- a/src/style.css +++ b/src/style.css @@ -44,5 +44,5 @@ @apply rounded-2xl bg-zinc-800 p-4 } .rounded-btn { - @apply rounded-full w-fit bg-blue-200 px-4 py-2 text-black font-medium + @apply rounded-full w-fit bg-blue-300 px-4 py-2 text-black fill-black font-medium cursor-pointer flex gap-1 } |
