Compare commits

..

6 Commits

Author SHA1 Message Date
58a096db29
Add Song model type 2025-11-13 00:20:32 +01:00
ef6e52c0cb
Add code to store search query and perform search 2025-11-13 00:20:02 +01:00
5066ce695d
Add rather dull favorite handling code 2025-11-13 00:18:54 +01:00
625a6d72f7
Add Base model type
Mostly copied from gitlab.com/schwalbe-hd/schwalbe

It allows both the names `id` and `uuid` for the identifier field of an instance
2025-11-13 00:17:42 +01:00
a5c50bd8f9
Add main UI js file 2025-11-13 00:16:41 +01:00
4fa29a9161
Reference old repo 2025-11-13 00:15:20 +01:00
6 changed files with 147 additions and 0 deletions

View File

@ -28,6 +28,8 @@ export SONG_LIBRARY=/path/to/library
**A web-based UI to browse the song library.** It supports searching by title and artist, and allows to mark and unmark favorites on the client side (using local storage).
The UI was developed in a different repository, available for historic reasons at https://gitlab.cl.uni-heidelberg.de/moser/karaokatalog-ui.
#### Serve
**Launch a server for the catalogue UI** on localhost. Mostly useful for development purposes.

View File

@ -0,0 +1,15 @@
import "./lib/mithril.min.js"
import All from "./components/pages/All.js"
import Favorites from "./components/pages/Favorites.js"
/*
* We leave the default routing strategy (https://mithril.js.org/route.html#routing-strategies), i.e.,
* with a #! prefix, and do not change m.route.prefix.
*
* This is to make it easier to deploy this app on servers without changing the configuration.
*/
m.route(document.body, "/all", {
"/all": All,
"/favorites": Favorites,
})

View File

@ -0,0 +1,64 @@
function toMap(objs) {
return new Map(objs.map(obj => [obj.id || obj.uuid, obj]))
}
export class Base {
/**
* A Map mapping ids to instances of `Base`.
* Must be treated as private.
*/
static _byId = null
/**
* @return A list of all instances of `Base`
*/
static get all() {
return this._byId ? Array.from(this._byId.values()) : null
}
/**
* @param {str} id A id
* @returns The instance with the given id, if it exists
*/
static get(id) {
return this._byId ? this._byId.get(id) : null
}
/**
* Load the objects from the API, unless the objects have already been loaded.
* They are then stored in `this.byUid`.
*
* @returns Nothing.
*/
static async load() {
if (!this._byId) {
const objects = await this.forceLoad()
// Hack: We get plain JSON objects from the server, however, we'd like
// them to actually be instances of the classes we define. So, we first create a new empty
// instance of whatever subclass of `Base` we are currently in using `new this()`,
// and then copy all properties from the plain object over to the instance
// using `Object.assign`.
const instances = objects.map(obj => Object.assign(new this(), obj))
this._byId = toMap(instances)
}
}
/**
* If the objects were already loaded, forget them and load them again.
* If the objects were not yet loaded, don't do anything.
*/
static refresh() {
if (this._byId) {
this._byId = null
this.load()
}
}
/**
* Load the objects from the API and return them.
* Abstract method, must be overriden in child classes.
*
* @returns A list of instances.
*/
static async forceLoad() {}
}

View File

@ -0,0 +1,24 @@
import { Base } from "./Base.js"
import { getFavoriteIds, addFavorite, removeFavorite } from "./favorites.js"
export default class Song extends Base {
static async forceLoad() {
return await m.request({ url: "/songs.json" })
}
get favorite() {
return getFavoriteIds().has(this.id || this.uuid)
}
set favorite(isFavorite) {
if (isFavorite) {
addFavorite(this.id || this.uuid)
} else {
removeFavorite(this.id || this.uuid)
}
}
toggleFavorite() {
this.favorite = !this.favorite
}
}

View File

@ -0,0 +1,32 @@
const FAVORITES_STORAGE_KEY = "songAppFavorites"
let favoriteIds = null
export function getFavoriteIds() {
if (favoriteIds === null) {
favoriteIds = new Set(
JSON.parse(localStorage.getItem(FAVORITES_STORAGE_KEY)) || [],
)
}
return favoriteIds
}
function setFavoriteIds(favs) {
localStorage.setItem(
FAVORITES_STORAGE_KEY,
JSON.stringify(Array.from(favs)),
)
}
export function addFavorite(id) {
const favs = getFavoriteIds()
favs.add(id)
setFavoriteIds(favs)
}
export function removeFavorite(id) {
const favs = getFavoriteIds()
favs.delete(id)
setFavoriteIds(favs)
}

View File

@ -0,0 +1,10 @@
export default {
query: null,
apply(songs) {
const normalizedQuery = this.query?.trim()?.toLowerCase()
return normalizedQuery ? songs?.filter(
song => song.title?.toLowerCase()?.includes(normalizedQuery) || song.artist?.toLowerCase()?.includes(normalizedQuery)
) : songs
}
}