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).
|
||||
|
||||
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.
|
||||
|
||||
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