Compare commits
6 Commits
e4c5806974
...
58a096db29
| Author | SHA1 | Date | |
|---|---|---|---|
| 58a096db29 | |||
| ef6e52c0cb | |||
| 5066ce695d | |||
| 625a6d72f7 | |||
| a5c50bd8f9 | |||
| 4fa29a9161 |
@ -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).
|
**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
|
#### Serve
|
||||||
|
|
||||||
**Launch a server for the catalogue UI** on localhost. Mostly useful for development purposes.
|
**Launch a server for the catalogue UI** on localhost. Mostly useful for development purposes.
|
||||||
|
|||||||
15
karaokatalog/ui/static/karaokatalog.js
Normal file
15
karaokatalog/ui/static/karaokatalog.js
Normal 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,
|
||||||
|
})
|
||||||
64
karaokatalog/ui/static/model/Base.js
Normal file
64
karaokatalog/ui/static/model/Base.js
Normal 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() {}
|
||||||
|
}
|
||||||
24
karaokatalog/ui/static/model/Song.js
Normal file
24
karaokatalog/ui/static/model/Song.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
32
karaokatalog/ui/static/model/favorites.js
Normal file
32
karaokatalog/ui/static/model/favorites.js
Normal 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)
|
||||||
|
}
|
||||||
10
karaokatalog/ui/static/model/search.js
Normal file
10
karaokatalog/ui/static/model/search.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user