diff --git a/public/player-marker.js b/public/player-marker.js index 5b3ab87..7c696f1 100644 --- a/public/player-marker.js +++ b/public/player-marker.js @@ -1,7 +1,9 @@ +let lastWidth = 0 function movePlayer() { const pc = document.getElementById("player-container") const playerExists = pc.querySelector("iframe") if (!playerExists) return + lastWidth = window.innerWidth const pm = document.getElementById("player-marker") pm.style.display = "block" pm.style.height = `${pc.clientHeight}px` @@ -9,12 +11,20 @@ function movePlayer() { } movePlayer() document.body.addEventListener("htmx:load", movePlayer) +window.addEventListener("resize", () => { + if ((window.innerWidth > 980 && lastWidth < 980) || (window.innerWidth < 980 && lastWidth > 980)) { + showPlayer() + movePlayer() + } +}) function addPopoverStyle() { document.querySelectorAll("[popovertarget]").forEach(e => { e.addEventListener("click", () => { const rect = e.getBoundingClientRect() - const t = `:popover-open { position: fixed; top: ${Math.floor(rect.bottom)}px; left: ${Math.floor(rect.left + rect.width / 2)}px; width: ${Math.floor(rect.width + 85)}px; transform: translateX(-50%); margin: 0 }` + const width = Math.floor(rect.width + 85) + const left = Math.max(Math.floor(rect.left + rect.width / 2), width / 2) + const t = `:popover-open { position: fixed; top: ${Math.floor(rect.bottom)}px; left: ${left}px; width: ${width}px; transform: translateX(-50%); margin: 0 }` document.styleSheets[0].insertRule(t, document.styleSheets[0].cssRules.length) }) }) @@ -23,9 +33,32 @@ addPopoverStyle() document.body.addEventListener("htmx:load", addPopoverStyle) document.body.addEventListener("htmx:beforeHistoryUpdate", o => { - console.log("beforeHistoryUpdate:", o) const page = document.getElementById("page") if (o?.detail?.requestConfig?.target === page) { while (page.firstChild) page.firstChild.remove() } }) + +function hidePlayer() { + document.getElementById("player-container").style.visibility = "hidden" + document.getElementById("toggle-player").setAttribute("aria-pressed", "false") + document.getElementById("toggle-player").classList.remove("is-selected") +} +function showPlayer() { + document.getElementById("player-container").style.visibility = "visible" + document.getElementById("toggle-player").setAttribute("aria-pressed", "true") + document.getElementById("toggle-player").classList.add("is-selected") +} +function togglePlayer() { + const pc = document.getElementById("player-container") + if (pc.style.visibility === "visible") hidePlayer() + else showPlayer() +} +function setupTogglePlayerButton(event) { + if (event?.target?.id === "player") showPlayer() + else hidePlayer() + document.getElementById("toggle-player").removeEventListener("click", togglePlayer) + document.getElementById("toggle-player").addEventListener("click", togglePlayer) +} +setupTogglePlayerButton() +document.body.addEventListener("htmx:load", setupTogglePlayerButton) diff --git a/public/style.css b/public/style.css index 57af210..f6adc80 100644 --- a/public/style.css +++ b/public/style.css @@ -1,5 +1,17 @@ .ws340 { width: 340px; + margin: auto; +} +@media screen and (max-width: 980px) { + .ws340 { + width: calc(100% - 32px); + max-width: 700px; /* bandcamp iframe body has this max-width */ + } +} +@media screen and (min-width: 981px) { + #player-container { + visibility: visible !important; + } } .themed { --theme-base-primary-color-h: 191; @@ -22,6 +34,12 @@ .s-navigation__toggle.s-navigation { --_na-item-bg: var(--black-150); } +@media screen and (max-width: 450px) { + .s-navigation__toggle.s-navigation .is-selected svg { + visibility: hidden; + width: 0; + } +} .duration-last-col td:last-child { text-align: right; white-space: pre; @@ -48,6 +66,28 @@ button.s-link.is-loading { .s-btn__dropdown:has(+ :popover-open) { background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important; } +@media screen and (max-width: 700px) { + .preview-cover:nth-of-type(n + 5) { + display: none !important; + } +} +@media screen and (max-width: 550px) { + .preview-cover:nth-of-type(n + 3) { + display: none !important; + } +} +.album-grid { + grid-template-columns: repeat(auto-fit, 210px) +} +@media screen and (max-width: 980px) { + .album-grid { + grid-template-columns: repeat(auto-fit, 150px); + } + .cover img { + width: 150px; + height: 150px; + } +} /* album covers are done with styles instead of attributes to reduce bytes of html needing to be downloaded and parsed */ .cover { @@ -71,17 +111,17 @@ button.s-link.is-loading { top: 50%; left: 50%; transform: translate(-50%, -50%); - transition: 1s ease-out; + transition: opacity 1s ease-out; color: var(--black); } .cover:hover svg { - transition: 1.5s ease-out 0.7s; + transition: opacity 1.5s ease-out 0.7s; opacity: 1; } .cover img { - transition: 1s ease-out; + transition: opacity 1s ease-out; } .cover:hover img { - transition: 2s ease-out 0.7s; + transition: opacity 2s ease-out 0.7s; opacity: 0.3; } diff --git a/pug/album_grid.pug b/pug/album_grid.pug index 4c01bb0..0268827 100644 --- a/pug/album_grid.pug +++ b/pug/album_grid.pug @@ -6,7 +6,7 @@ block title block view .mx-auto.w100.wmx11.fs-body1#content != icons.useTemplate(["star-solid", "play-solid", "music-note", "compact-disc", "people-tag", "flower"]) - .d-grid.gx8.gy12.jc-center.break-word(style="grid-template-columns: repeat(auto-fit, 210px)") + .d-grid.gx8.gy12.jc-center.break-word.album-grid each item in items div a.cover&attributes(getAlbumCoverAttributes(event, item)) @@ -16,7 +16,7 @@ block view p= item.item_title .d-flex.fw-wrap.g4 if item.why - a.s-tag.s-tag__xs.fc-orange-400(title=item.why href=and({filter_field: "why", filter: "reviewed"})) + a.s-tag.s-tag__xs.fc-orange-400(title=(item.why + (item.featured_track_title ? ` -- favourite track: ${item.featured_track_title}` : "")) href=and({filter_field: "why", filter: "reviewed"})) != icons.use("star-solid", 16) if hasFullTrackData a.s-tag.s-tag__xs(href=and({arrange: "track", filter_field: "item_id", filter: item.item_id, filter_fuzzy: null})) diff --git a/pug/artist_grid.pug b/pug/artist_grid.pug index a4d35e4..5907b72 100644 --- a/pug/artist_grid.pug +++ b/pug/artist_grid.pug @@ -10,7 +10,7 @@ block view each item in items .d-flex.g4 .fl-grow1.pb12 - .fs-headline1= item.band_name + .fs-headline1.break-word= item.band_name .d-flex.fw-wrap.g4 a.s-tag.s-tag__xs(href=and({arrange: "album", filter_field: "band_name", filter: item.band_name, filter_fuzzy: null})) span.s-tag--sponsor!= icons.use("album", 16) @@ -28,5 +28,5 @@ block view span.s-tag--sponsor!= icons.use("flower", 16) = label each preview in item.previews - a.d-flex&attributes(getAlbumCoverAttributes(event, preview)) + a.d-flex.preview-cover(title=preview.item_title)&attributes(getAlbumCoverAttributes(event, preview)) img(loading="lazy" src=preview.item_art_url width=210 height=210 style="height: 70px; width: 70px;") diff --git a/pug/collection-stats.pug b/pug/collection-stats.pug new file mode 100644 index 0000000..cd47381 --- /dev/null +++ b/pug/collection-stats.pug @@ -0,0 +1,5 @@ +extends includes/layout.pug + +block view + .mx-auto.w100.wmx11.fs-body1#content + include includes/collection-stats.pug diff --git a/pug/home.pug b/pug/home.pug index c9fe400..5d33358 100644 --- a/pug/home.pug +++ b/pug/home.pug @@ -5,7 +5,7 @@ html title BC Explorer link(rel="icon" href="/favicon.png") - link(rel="stylesheet" type="text/css" href="/static/stacks.min.css") + link(rel="stylesheet" type="text/css" href="/static/stacks.css") script(src="/static/htmx.js") meta(name="htmx-config" content='{"requestClass":"is-loading"}') body.themed.theme-system.overflow-y-scroll diff --git a/pug/includes/layout.pug b/pug/includes/layout.pug index ffaf5ef..b68f932 100644 --- a/pug/includes/layout.pug +++ b/pug/includes/layout.pug @@ -1,5 +1,5 @@ mixin navi(key, value, icon, text) - a.s-navigation--item(href=and({[key]: value}) class={"is-selected": query[key] === value})&attributes(attributes) + a.s-navigation--item(href="./" + and({shape: query && query.shape || "grid", [key]: value}) class={"is-selected": query && query[key] === value})&attributes(attributes) if icon != icons.get(icon) if text @@ -19,7 +19,7 @@ html - title = `${searchText} | ${title}` title#title= title link(rel="icon" href="/favicon.png") - link(rel="stylesheet" type="text/css" href="/static/stacks.min.css") + link(rel="stylesheet" type="text/css" href="/static/stacks.css") link(rel="stylesheet" type="text/css" href="/static/style.css") script(src="/static/htmx.js") script(src="/static/wordcloud.js") @@ -60,7 +60,7 @@ html each currency in currencies option(selected=(currency === count.displayCurrency))= currency .fl-grow1 - nav + nav.d-block(class="md:d-none") ul.s-navigation li: +navi("arrange", "album", "album", "Album") li: +navi("arrange", "artist", "people-tag", "Artist") @@ -68,16 +68,38 @@ html //- asana, flower, component, circle-spark, rhombus, sphere, union-alt, color-wheel, community, combine li: +navi("arrange", "tag", "label", "Tag") li: +navi("arrange", "track", "music-note", "Track") - .px16 - nav - ul.s-navigation.s-navigation__toggle.g0 - li: +navi("shape", "grid").brr0!= icons.get("view-grid") - li: +navi("shape", "list").blr0!= icons.get("align-justify") + li.d-none(class="md:d-block") + a.s-navigation--item(href="collection-stats" class={"is-selected": isStatsPage}) + != icons.get("graph-up") + span.ml4 Stats + ul.s-navigation.s-navigation__toggle.d-none(class="md:d-block") + button.s-navigation--item.s-navigation--item__dropdown(popovertarget="arranges") + != icons.get("lens") + span.ml4 Arrange + #arranges(popover data-popper-placement="bottom" style="display: revert;").s-popover.overflow-visible + .s-popover--arrow.s-popover--arrow__tc + ul.s-navigation.s-navigation__vertical + li: +navi("arrange", "album", "album", "Album") + li: +navi("arrange", "artist", "people-tag", "Artist") + li: +navi("arrange", "label", "flower", "Label") + li: +navi("arrange", "tag", "label", "Tag") + li: +navi("arrange", "track", "music-note", "Track") + li + a.s-navigation--item(href="collection-stats" class={"is-selected": isStatsPage}) + != icons.get("graph-up") + span.ml4 Stats + if !isStatsPage + .px16(class="md:px4") + nav + ul.s-navigation.s-navigation__toggle.g0 + li: +navi("shape", "grid").brr0!= icons.get("view-grid") + li: +navi("shape", "list").blr0!= icons.get("align-justify") .fl-grow1 + button#toggle-player.s-btn.s-btn__xs.d-none.mr4(class="md:d-block")!= icons.get("playlist") - .d-flex.py24.px16.g24.fs-body1.fd-row-reverse + .d-flex.py24.px16.g24.fs-body1.fd-row-reverse(class="md:fd-column") aside.ws340.fl-shrink0 - .ps-fixed.ws340.d-flex.fd-column.g12(style="top: 80px") + .ps-fixed.ws340.d-flex.fd-column.g12(class="md:ps-static md:jc-center" style="top: 80px") if arrange === "tag" include tag-status.pug @@ -99,14 +121,15 @@ html input(type="hidden" name="shape" value=shape) button.s-btn.s-btn__xs.s-btn__icon.s-btn__outlined.s-btn__muted#search-submit(style="height: 38px")!= icons.get("search") - #player-marker.pe-none(style="display: none") + #player-marker.pe-none(class="md:d-none" style="display: none") #collection-sync.d-none - include collection-stats.pug + div(class="md:d-none") + include collection-stats.pug main.fl-grow1 block view - #player-container.ps-fixed.r16.ws340(hx-preserve="true") + #player-container.ps-fixed.r16.ws340.z-modal(class="md:t64 md:l16 md:r16 md:b16" hx-preserve="true") #player diff --git a/pug/includes/tag-status.pug b/pug/includes/tag-status.pug index ec30bca..8af5bd7 100644 --- a/pug/includes/tag-status.pug +++ b/pug/includes/tag-status.pug @@ -28,7 +28,7 @@ if downloader.total > 0 || downloader.outcome != icons.get("cloud-check") .fl-grow1 Tags downloaded. - downloader.resolve() - a.s-btn.s-btn__outlined(href="") Refresh + a.s-btn.s-btn__outlined(href=and({arrange: "tag"}) hx-boost="true") Refresh else .s-notice.s-notice__danger.p8.gx16.pl16.d-flex.ai-center diff --git a/pug/label_grid.pug b/pug/label_grid.pug index cc8b58a..c111ed7 100644 --- a/pug/label_grid.pug +++ b/pug/label_grid.pug @@ -12,7 +12,7 @@ block view .fl-grow1.pb12 - let minBandURL = item.band_url.replace(/https?:\/\/(.*?)\.bandcamp\.com.*/, "$1") - let label = item.display_name.replace(/https?:\/\/(.*?)\.bandcamp\.com.*/, "$1") - .fs-headline1= label + .fs-headline1.break-word= label .d-flex.fw-wrap.g4 a.s-tag.s-tag__xs(href=and({arrange: "album", filter_field: "band_url", filter: minBandURL, filter_fuzzy: null})) span.s-tag--sponsor!= icons.use("album", 16) @@ -28,5 +28,5 @@ block view span.s-tag--sponsor!= icons.use("compact-disc", 16) = item.total_duration each preview in item.previews - a.d-flex&attributes(getAlbumCoverAttributes(event, preview)) - img(loading="lazy" src=preview.item_art_url width=210 height=210 style="height: auto; width: auto; max-height: 70px") + a.d-flex.preview-cover(title=preview.item_title)&attributes(getAlbumCoverAttributes(event, preview)) + img(loading="lazy" src=preview.item_art_url width=210 height=210 style="height: 70px; width: 70px;") diff --git a/routes/_index.js b/routes/_index.js index e6220b7..b16468f 100644 --- a/routes/_index.js +++ b/routes/_index.js @@ -18,7 +18,7 @@ sync.require("./settings") // Files -router.get("/static/stacks.min.css", defineEventHandler({ +router.get("/static/stacks.css", defineEventHandler({ onBeforeResponse: pugSync.compressResponse, handler: async event => { handleCacheHeaders(event, {maxAge: 86400}) @@ -65,7 +65,7 @@ router.get("/static/player-marker.js", defineEventHandler({ router.get("/favicon.png", defineEventHandler({ handler: async event => { handleCacheHeaders(event, {maxAge: 86400}) - defaultContentType(event, "text/javascript") + defaultContentType(event, "image/png") return fs.promises.readFile("public/favicon.png") } })) diff --git a/routes/app.js b/routes/app.js index 3a176ef..5551fe9 100644 --- a/routes/app.js +++ b/routes/app.js @@ -1,14 +1,13 @@ // @ts-check -const {z} = require("zod") const {defineEventHandler, getQuery, getValidatedQuery, sendRedirect, createError, getValidatedRouterParams, getCookie} = require("h3") -const {router, db, sync, select, from} = require("../passthrough") +const {router, db, sync} = require("../passthrough") /** @type {import("../pug-sync")} */ const pugSync = sync.require("../pug-sync") -/** @type {import("./load-tags")} */ -const loadTags = sync.require("./load-tags") +/** @type {import("./schema")} */ +const schema = sync.require("./schema") const sqls = { album_grid: "SELECT item.*, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (account, item_id) {JOIN TAG} WHERE account = ? {WHERE} GROUP BY item_id {ORDER}", @@ -24,7 +23,7 @@ const sqls = { function loadPreviews(locals, field, number, whereClause, account, filter_field, filter, filter_fuzzy) { const params = [account, number] - let sql = `SELECT ${field}, item_id, item_type, item_url, item_art_url FROM (SELECT ${field}, item_id, item_type, item_url, item_art_url, row_number() OVER (PARTITION BY ${field} ORDER BY purchased DESC) AS row_number FROM item {JOIN TAG} WHERE account = ? {WHERE}) WHERE row_number <= ?` + let sql = `SELECT ${field}, item_id, item_title, item_type, item_url, item_art_url FROM (SELECT ${field}, item_title, item_id, item_type, item_url, item_art_url, row_number() OVER (PARTITION BY ${field} ORDER BY purchased DESC) AS row_number FROM item {JOIN TAG} WHERE account = ? {WHERE}) WHERE row_number <= ?` sql = sql.replace("{WHERE}", whereClause) if (whereClause) { if (filter_field === "band_url" || filter_fuzzy) { @@ -69,25 +68,12 @@ pugSync.addGlobals({ } }) -const schema = { - query: z.object({ - arrange: z.enum(["album", "artist", "label", "tag", "track"]), - shape: z.enum(["grid", "list"]), - filter_field: z.enum(["band_name", "band_url", "item_title", "item_id", "tag", "why"]).optional(), - filter: z.string().optional(), - filter_fuzzy: z.enum(["true"]).optional() - }), - params: z.object({ - account: z.string() - }) -} - router.get("/:account/", defineEventHandler({ onBeforeResponse: pugSync.compressResponse, handler: async event => { + const {account} = await getValidatedRouterParams(event, schema.schema.account.parse) try { - var {account} = await getValidatedRouterParams(event, schema.params.parse) - var {arrange, shape, filter, filter_field, filter_fuzzy} = await getValidatedQuery(event, schema.query.parse) + var {arrange, shape, filter, filter_field, filter_fuzzy} = await getValidatedQuery(event, schema.schema.appQuery.parse) if (filter_field === "why" && arrange !== "album") throw new Error("filter not compatible with arrangement") } catch (e) { return sendRedirect(event, "?arrange=album&shape=grid", 302) diff --git a/routes/collection-stats.js b/routes/collection-stats.js index 547fdd8..85e371f 100644 --- a/routes/collection-stats.js +++ b/routes/collection-stats.js @@ -1,12 +1,14 @@ // @ts-check -const {getCookie, defineEventHandler, readValidatedBody, setCookie} = require("h3") -const {z} = require("zod") +const {getCookie, defineEventHandler, readValidatedBody, setCookie, getValidatedRouterParams} = require("h3") const {sync, select, db, router} = require("../passthrough") /** @type {import("../pug-sync")} */ const pugSync = sync.require("../pug-sync") +/** @type {import("./schema")} */ +const schema = sync.require("./schema") + const currencyExchange = new Map([ ["AUD", 0.63], ["BRL", 0.17], @@ -21,6 +23,12 @@ const currencyExchange = new Map([ ]) const currencies = [...currencyExchange.keys()] +pugSync.beforeInclude("includes/layout.pug", async (from, event, locals) => { + return { + currencies + } +}) + pugSync.beforeInclude("includes/collection-stats.pug", async (from, event, {account, currency}) => { let displayCurrency = currency || getCookie(event, "bcex-currency") || "" if (!currencyExchange.has(displayCurrency)) displayCurrency = "NZD" @@ -44,20 +52,17 @@ pugSync.beforeInclude("includes/collection-stats.pug", async (from, event, {acco }).reduce((a, c) => a + c, 0)) * currencyRoundTo, displayCurrency, taste: db.prepare("with popularity (a) as (select avg(also_collected_count) from item WHERE account = ? group by band_url) select sum(iif(a >= 0 and a < 20, 1, 0)) as cold, sum(iif(a >= 20 and a < 200, 1, 0)) as warm, sum(iif(a >= 200 and a < 2000, 1, 0)) as hot, sum(iif(a >= 2000, 1, 0)) as supernova from popularity").raw().get(account) - }, - currencies + } } }) -const schema = { - currency: z.object({ - currency: z.string().regex(/^[A-Z]{3}$/), - account: z.string() - }) -} +router.get("/:account/collection-stats", defineEventHandler(async event => { + const {account} = await getValidatedRouterParams(event, schema.schema.account.parse) + return pugSync.render(event, "collection-stats.pug", {account, isStatsPage: true}) +})) router.post("/api/settings/currency", defineEventHandler(async event => { - const {currency, account} = await readValidatedBody(event, schema.currency.parse) + const {currency, account} = await readValidatedBody(event, schema.schema.postCurrency.parse) setCookie(event, "bcex-currency", currency) return pugSync.render(event, "includes/collection-stats.pug", {account, currency}) })) diff --git a/routes/load-collection.js b/routes/load-collection.js index 4bfb9a1..a8b77d1 100644 --- a/routes/load-collection.js +++ b/routes/load-collection.js @@ -1,23 +1,26 @@ // @ts-check const assert = require("assert/strict") -const fs = require("fs") -const sqlite = require("better-sqlite3") const domino = require("domino") const {defineEventHandler, readValidatedBody, setCookie, getCookie} = require("h3") -const {z} = require("zod") const {sync, db, router} = require("../passthrough") + +/** @type {import("../pug-sync")} */ const pugSync = sync.require("../pug-sync") + /** @type {import("./load-tags")} */ const loadTags = sync.require("./load-tags") +/** @type {import("./schema")} */ +const schema = sync.require("./schema") + async function loadCollection(inputUsername) { assert.match(inputUsername, /^[a-z0-9_-]+$/) const html = await fetch(`https://bandcamp.com/${inputUsername}`).then(res => res.text()) const doc = domino.createDocument(html) - const first = doc.querySelector(".collection-item-container") + const first = doc.querySelector(".collection-item-container[data-token]") assert(first) const token = first.getAttribute("data-token") assert(token) @@ -49,6 +52,7 @@ async function loadCollection(inputUsername) { const preparedItem = db.prepare(`INSERT INTO item (${columns.join(", ")}) VALUES (${columns.map(x => "@" + x).join(", ")}) ON CONFLICT DO UPDATE SET ${upsert_columns.map(x => `${x} = @${x}`).join(", ")}`) db.transaction(() => { for (const item of items.items) { + if (!item.tralbum_type.match(/[at]/)) continue // p=product and s=subscription not supported preparedItem.run({ account, ...item, @@ -88,12 +92,8 @@ async function loadCollection(inputUsername) { } } -const schema = z.object({ - account: z.string() -}) - router.post("/api/load-collection", defineEventHandler(async event => { - const {account} = await readValidatedBody(event, schema.parse) + const {account} = await readValidatedBody(event, schema.schema.account.parse) const result = await loadCollection(account) setCookie(event, "accounts", (getCookie(event, "accounts") || "").split("|").concat(account).join("|")) return pugSync.render(event, "collection-loaded.pug", result) diff --git a/routes/load-tags.js b/routes/load-tags.js index a4ae3ea..91991c5 100644 --- a/routes/load-tags.js +++ b/routes/load-tags.js @@ -1,12 +1,16 @@ // @ts-check const domino = require("domino") -const {z} = require("zod") const {getValidatedQuery, readValidatedBody, defineEventHandler} = require("h3") -const {sync, db, from, router} = require("../passthrough") +const {sync, db, router} = require("../passthrough") + +/** @type {import("../pug-sync")} */ const pugSync = sync.require("../pug-sync") +/** @type {import("./schema")} */ +const schema = sync.require("./schema") + const insertTag = db.prepare("INSERT OR IGNORE INTO item_tag (account, item_id, tag) VALUES (?, ?, ?)") const insertTrack = db.prepare("INSERT OR IGNORE INTO track (account, item_id, track_id, title, artist, track_number, duration) VALUES (@account, @item_id, @track_id, @title, @artist, @track_number, @duration)") @@ -118,17 +122,13 @@ const downloadManager = new class { } } -const schema = z.object({ - account: z.string() -}) - router.get("/api/tag-download", defineEventHandler(async event => { - const {account} = await getValidatedQuery(event, schema.parse) + const {account} = await getValidatedQuery(event, schema.schema.account.parse) return pugSync.render(event, "includes/tag-status.pug", {account}) })) router.post("/api/tag-download", defineEventHandler(async event => { - const {account} = await readValidatedBody(event, schema.parse) + const {account} = await readValidatedBody(event, schema.schema.account.parse) downloadManager.start(account) return pugSync.render(event, "includes/tag-status.pug", {account}) })) diff --git a/routes/play.js b/routes/play.js index bfe5339..294e526 100644 --- a/routes/play.js +++ b/routes/play.js @@ -1,6 +1,5 @@ // @ts-check -const {z} = require("zod") const {sync, router} = require("../passthrough") const {defineEventHandler} = require("h3") const {getValidatedRouterParams} = require("h3") @@ -8,14 +7,11 @@ const {getValidatedRouterParams} = require("h3") /** @type {import("../pug-sync")} */ const pugSync = sync.require("../pug-sync") -const schema = z.object({ - item_type: z.enum(["album", "track"]), - item_id: z.number({coerce: true}), - track_id: z.number({coerce: true}).optional() -}) +/** @type {import("./schema")} */ +const schema = sync.require("./schema") const play = defineEventHandler(async event => { - const locals = await getValidatedRouterParams(event, schema.parse) + const locals = await getValidatedRouterParams(event, schema.schema.play.parse) return pugSync.render(event, "player.pug", locals) }) diff --git a/routes/schema.js b/routes/schema.js new file mode 100644 index 0000000..a690bd3 --- /dev/null +++ b/routes/schema.js @@ -0,0 +1,30 @@ +// @ts-check + +const {z} = require("zod") + +const schema = { + appQuery: z.object({ + arrange: z.enum(["album", "artist", "label", "tag", "track"]), + shape: z.enum(["grid", "list"]), + filter_field: z.enum(["band_name", "band_url", "item_title", "item_id", "tag", "why"]).optional(), + filter: z.string().optional(), + filter_fuzzy: z.enum(["true"]).optional() + }), + account: z.object({ + account: z.string().regex(/^[a-z0-9_-]+$/) + }), + postCurrency: z.object({ + currency: z.string().regex(/^[A-Z]{3}$/), + account: z.string() + }), + play: z.object({ + item_type: z.enum(["album", "track"]), + item_id: z.number({coerce: true}), + track_id: z.number({coerce: true}).optional() + }), + inlinePlayer: z.object({ + inline_player: z.string().optional() + }) +} + +module.exports.schema = schema diff --git a/routes/settings.js b/routes/settings.js index f492b27..207619e 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -1,17 +1,13 @@ // @ts-check -const {z} = require("zod") -const {router} = require("../passthrough") +const {sync, router} = require("../passthrough") const {defineEventHandler, readValidatedBody, setCookie, setResponseHeader} = require("h3") -const schema = { - inline_player: z.object({ - inline_player: z.string().optional() - }) -} +/** @type {import("./schema")} */ +const schema = sync.require("./schema") router.post("/api/settings/inline-player", defineEventHandler(async event => { - const {inline_player} = await readValidatedBody(event, schema.inline_player.parse) + const {inline_player} = await readValidatedBody(event, schema.schema.inlinePlayer.parse) setCookie(event, "bcex-inline-player-disabled", String(!inline_player)) setResponseHeader(event, "HX-Refresh", "true") return null