diff --git a/.vscode/settings.json b/.vscode/settings.json index c36ff6a..11caac5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "svelte.enable-ts-plugin": true, - "editor.tabSize": 2 -} \ No newline at end of file + "svelte.enable-ts-plugin": true, + "editor.tabSize": 2 +} diff --git a/README.md b/README.md index 7f29c0e..3ab7ce4 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,4 @@ A production build is also available at `packages.x86_64-linux.musikspider`. - [ ] implement persistence of music data, playlists and such - [ ] implement scrobbling (last.fm, etc) - [ ] implement discord status -- [ ] add tauri app? \ No newline at end of file +- [ ] add tauri app? diff --git a/postcss.config.js b/postcss.config.js index 2e7af2b..0f77216 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,6 @@ export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/src/app.d.ts b/src/app.d.ts index 2bc1360..824da68 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -11,4 +11,4 @@ declare global { } } -export { }; +export {}; diff --git a/src/app.html b/src/app.html index cbf4f24..fc09c3e 100644 --- a/src/app.html +++ b/src/app.html @@ -1,15 +1,13 @@ + + + + + %sveltekit.head% + - - - - - %sveltekit.head% - - - -
%sveltekit.body%
- - - \ No newline at end of file + +
%sveltekit.body%
+ + diff --git a/src/app.postcss b/src/app.postcss index e447f71..ce2258e 100644 --- a/src/app.postcss +++ b/src/app.postcss @@ -1,2 +1,7 @@ -html, body { @apply h-full overflow-hidden; } -.card { @apply shadow shadow-black; } \ No newline at end of file +html, +body { + @apply h-full overflow-hidden; +} +.card { + @apply shadow shadow-black; +} diff --git a/src/comms.ts b/src/comms.ts index f52286c..45cc9bf 100644 --- a/src/comms.ts +++ b/src/comms.ts @@ -1,174 +1,176 @@ import { dev } from '$app/environment'; -import type { TrackWithId } from "./types"; +import type { TrackWithId } from './types'; const API_VERSION: number = 20; const HTTP_DISABLED_ERROR: string = - "server does not have HTTP resources enabled, you will not be able to stream music"; -const SERVER_API_INCOMPATIBLE_ERROR: (serverApi: number) => string = - (serverApi) => `server API version (${serverApi}) is different from our supported version (${API_VERSION})`; + 'server does not have HTTP resources enabled, you will not be able to stream music'; +const SERVER_API_INCOMPATIBLE_ERROR: (serverApi: number) => string = (serverApi) => + `server API version (${serverApi}) is different from our supported version (${API_VERSION})`; interface Message { - name: string; - id: string; - device_id: string; - type: MessageType; - options: any; -}; + name: string; + id: string; + device_id: string; + type: MessageType; + options: any; +} type MessageType = 'request' | 'response' | 'broadcast'; type RequestCallback = (arg0: Message | null) => void; interface Callbacks { - onDisconnect: (authenticated: boolean, reason: string) => void; - onConnect: (initial: Message) => void; - onIncompatible: (reason: string) => void; + onDisconnect: (authenticated: boolean, reason: string) => void; + onConnect: (initial: Message) => void; + onIncompatible: (reason: string) => void; } export class MetadataCommunicator { - ws: WebSocket | null; - deviceId: string; - callbacks: Map; - authenticated: boolean; - eventCallbacks: Callbacks; - onConnectCallbacks: (() => void)[]; + ws: WebSocket | null; + deviceId: string; + callbacks: Map; + authenticated: boolean; + eventCallbacks: Callbacks; + onConnectCallbacks: (() => void)[]; - constructor() { - this.callbacks = new Map(); - this.deviceId = crypto.randomUUID(); - this.authenticated = false; - this.eventCallbacks = { - onDisconnect: () => { }, - onConnect: () => { }, - onIncompatible: () => { }, - }; - this.onConnectCallbacks = []; - this.ws = null; - } + constructor() { + this.callbacks = new Map(); + this.deviceId = crypto.randomUUID(); + this.authenticated = false; + this.eventCallbacks = { + onDisconnect: () => {}, + onConnect: () => {}, + onIncompatible: () => {} + }; + this.onConnectCallbacks = []; + this.ws = null; + } - setCallbacks(callbacks: Callbacks) { - this.eventCallbacks = callbacks; - } + setCallbacks(callbacks: Callbacks) { + this.eventCallbacks = callbacks; + } - connect(address: string, password: string) { - this.close(); + connect(address: string, password: string) { + this.close(); - const scheme = dev ? "ws" : "wss"; - this.ws = new WebSocket(`${scheme}://${address}`); + const scheme = dev ? 'ws' : 'wss'; + this.ws = new WebSocket(`${scheme}://${address}`); - this.ws.addEventListener('open', (event) => { - this.makeRequest("authenticate", 'request', { password }, (msg) => { - if (msg!.options.authenticated) { - this.authenticated = true; - this.eventCallbacks.onConnect(msg!); - this.onConnectCallbacks.forEach((f) => f()); - this.onConnectCallbacks = []; - if (!msg!.options.environment.http_server_enabled) { - this.eventCallbacks.onIncompatible(HTTP_DISABLED_ERROR); - } - const serverApiVersion = msg!.options.environment.api_version; - if (serverApiVersion != API_VERSION) { - this.eventCallbacks.onIncompatible(SERVER_API_INCOMPATIBLE_ERROR(serverApiVersion)); - } - } - }); - }); - this.ws.addEventListener('close', (event) => { - this.eventCallbacks.onDisconnect(this.authenticated, `${event.reason} (code ${event.code})`); - this.authenticated = false; - }); + this.ws.addEventListener('open', (event) => { + this.makeRequest('authenticate', 'request', { password }, (msg) => { + if (msg!.options.authenticated) { + this.authenticated = true; + this.eventCallbacks.onConnect(msg!); + this.onConnectCallbacks.forEach((f) => f()); + this.onConnectCallbacks = []; + if (!msg!.options.environment.http_server_enabled) { + this.eventCallbacks.onIncompatible(HTTP_DISABLED_ERROR); + } + const serverApiVersion = msg!.options.environment.api_version; + if (serverApiVersion != API_VERSION) { + this.eventCallbacks.onIncompatible(SERVER_API_INCOMPATIBLE_ERROR(serverApiVersion)); + } + } + }); + }); + this.ws.addEventListener('close', (event) => { + this.eventCallbacks.onDisconnect(this.authenticated, `${event.reason} (code ${event.code})`); + this.authenticated = false; + }); - this.ws.addEventListener('message', (event) => { - const parsed: Message = JSON.parse(event.data); - const maybeCallback = this.callbacks.get(parsed.id); - if (maybeCallback) { - maybeCallback(parsed); - this.callbacks.delete(parsed.id); - } - }); - } + this.ws.addEventListener('message', (event) => { + const parsed: Message = JSON.parse(event.data); + const maybeCallback = this.callbacks.get(parsed.id); + if (maybeCallback) { + maybeCallback(parsed); + this.callbacks.delete(parsed.id); + } + }); + } - fetchTracksCount(): Promise { - const options = { count_only: true }; - const th = this; - return new Promise(function (resolve, reject) { - th.makeRequest("query_tracks", "request", options, (resp) => { - if (resp) { - resolve(resp.options.count); - } else { - reject(null); - } - }); - }); - } + fetchTracksCount(): Promise { + const options = { count_only: true }; + const th = this; + return new Promise(function (resolve, reject) { + th.makeRequest('query_tracks', 'request', options, (resp) => { + if (resp) { + resolve(resp.options.count); + } else { + reject(null); + } + }); + }); + } - fetchTracks(limit: number, offset: number, filter: string | null = null): Promise { - const options: any = { limit, offset }; - if (filter !== null) options.filter = filter; + fetchTracks(limit: number, offset: number, filter: string | null = null): Promise { + const options: any = { limit, offset }; + if (filter !== null) options.filter = filter; - const th = this; - return new Promise(function (resolve, reject) { - th.makeRequest("query_tracks", "request", options, (resp) => { - if (resp) { - const data: any[] = resp.options.data; - resolve(data.map((t) => ({ - id: t.external_id, - track: { - id: t.id, - title: t.title, - track_num: t.track, - album_title: t.album, - album_id: t.album_id, - artist_name: t.artist, - artist_id: t.artist_id, - thumbnail_id: t.thumbnail_id, - } - }))); - } else { - reject(null); - } - }); - }); - } + const th = this; + return new Promise(function (resolve, reject) { + th.makeRequest('query_tracks', 'request', options, (resp) => { + if (resp) { + const data: any[] = resp.options.data; + resolve( + data.map((t) => ({ + id: t.external_id, + track: { + id: t.id, + title: t.title, + track_num: t.track, + album_title: t.album, + album_id: t.album_id, + artist_name: t.artist, + artist_id: t.artist_id, + thumbnail_id: t.thumbnail_id + } + })) + ); + } else { + reject(null); + } + }); + }); + } - private makeRequest(name: string, type: MessageType, options: object, callback: RequestCallback) { - // return if not authenticated, allow authentication messages - if (this.isClosed() || !this.authenticated && name != "authenticate") { - callback(null); - return; - } - // Unique enough for our purposes (as request ID) - const id = Math.random().toString(36).substring(2) + Date.now().toString(36); - this.callbacks.set(id, callback); - const payload = JSON.stringify({ - name, - type, - options, - device_id: this.deviceId, - id, - }); - console.trace("sending metadata message: " + payload); - this.ws!.send(payload); - } + private makeRequest(name: string, type: MessageType, options: object, callback: RequestCallback) { + // return if not authenticated, allow authentication messages + if (this.isClosed() || (!this.authenticated && name != 'authenticate')) { + callback(null); + return; + } + // Unique enough for our purposes (as request ID) + const id = Math.random().toString(36).substring(2) + Date.now().toString(36); + this.callbacks.set(id, callback); + const payload = JSON.stringify({ + name, + type, + options, + device_id: this.deviceId, + id + }); + console.trace('sending metadata message: ' + payload); + this.ws!.send(payload); + } - close() { - if (this.isClosed()) return; - this.ws!.close(); - this.authenticated = false; - this.callbacks.clear(); - } + close() { + if (this.isClosed()) return; + this.ws!.close(); + this.authenticated = false; + this.callbacks.clear(); + } - isClosed() { - return ( - this.ws === null - || this.ws.readyState === WebSocket.CLOSED - || this.ws.readyState === WebSocket.CLOSING - ); - } + isClosed() { + return ( + this.ws === null || + this.ws.readyState === WebSocket.CLOSED || + this.ws.readyState === WebSocket.CLOSING + ); + } - onConnect(cb: () => Promise) { - if (!this.isClosed() && this.authenticated) { - cb(); - } else { - this.onConnectCallbacks = [...this.onConnectCallbacks, cb]; - } - } -} \ No newline at end of file + onConnect(cb: () => Promise) { + if (!this.isClosed() && this.authenticated) { + cb(); + } else { + this.onConnectCallbacks = [...this.onConnectCallbacks, cb]; + } + } +} diff --git a/src/components/track.svelte b/src/components/track.svelte index 720df94..b1e6785 100644 --- a/src/components/track.svelte +++ b/src/components/track.svelte @@ -4,14 +4,13 @@ makeThumbnailUrl, currentTrack, setQueuePositionTo, - makeGenScopedTokenUrl, - makeShareUrl + makeGenScopedTokenUrl } from '../stores'; import Spinnny from '~icons/line-md/loading-loop'; import IconPlay from '~icons/mdi/play'; import IconMusic from '~icons/mdi/music'; import { toastStore } from '@skeletonlabs/skeleton'; - import { getAudioElement } from '../utils'; + import { getAudioElement, makeShareUrl } from '../utils'; export let track_with_id: TrackWithId; let track = track_with_id.track; diff --git a/src/routes/(app)/+layout.ts b/src/routes/(app)/+layout.ts index a26ebff..778589c 100644 --- a/src/routes/(app)/+layout.ts +++ b/src/routes/(app)/+layout.ts @@ -1,5 +1,5 @@ -import { MetadataCommunicator } from "../../comms"; +import { MetadataCommunicator } from '../../comms'; export const _metadataComm = new MetadataCommunicator(); export const ssr = false; -export const prerender = true; \ No newline at end of file +export const prerender = true; diff --git a/src/routes/share/[token]/+layout.ts b/src/routes/share/[token]/+layout.ts index bf2831b..83addb7 100644 --- a/src/routes/share/[token]/+layout.ts +++ b/src/routes/share/[token]/+layout.ts @@ -1,2 +1,2 @@ export const ssr = false; -export const prerender = false; \ No newline at end of file +export const prerender = false; diff --git a/src/routes/share/[token]/+page.server.ts b/src/routes/share/[token]/+page.server.ts index 6d3bfb6..fbd7a1d 100644 --- a/src/routes/share/[token]/+page.server.ts +++ b/src/routes/share/[token]/+page.server.ts @@ -3,17 +3,17 @@ import { LOCAL_MUSIKQUAD_SERVER } from '$env/static/private'; import { scheme } from '../../../utils'; interface MusicInfo { - title: string, - album: string, - artist: string, + title: string; + album: string; + artist: string; } export async function load({ params }) { - const resp = await fetch(`${LOCAL_MUSIKQUAD_SERVER}/share/info/${params.token}`); - const info: MusicInfo = await resp.json(); - return { - info, - thumbnail_url: `${scheme}://${PUBLIC_MUSIKQUAD_SERVER}/share/thumbnail/${params.token}`, - audio_url: `${scheme}://${PUBLIC_MUSIKQUAD_SERVER}/share/audio/${params.token}`, - }; -} \ No newline at end of file + const resp = await fetch(`${LOCAL_MUSIKQUAD_SERVER}/share/info/${params.token}`); + const info: MusicInfo = await resp.json(); + return { + info, + thumbnail_url: `${scheme}://${PUBLIC_MUSIKQUAD_SERVER}/share/thumbnail/${params.token}`, + audio_url: `${scheme}://${PUBLIC_MUSIKQUAD_SERVER}/share/audio/${params.token}` + }; +} diff --git a/src/stores.ts b/src/stores.ts index 5f30577..b9dd4f0 100644 --- a/src/stores.ts +++ b/src/stores.ts @@ -1,55 +1,55 @@ import { get, writable } from 'svelte/store'; import { type Track, type TrackId, type TrackWithId, LoopKind } from './types'; -import { PUBLIC_BASEURL, PUBLIC_MUSIKQUAD_SERVER } from '$env/static/public'; +import { PUBLIC_MUSIKQUAD_SERVER } from '$env/static/public'; import { scheme } from './utils'; function writableStorage(key: string, defaultValue: string) { - const store = writable(localStorage.getItem(key) ?? defaultValue); - store.subscribe(value => localStorage.setItem(key, value)); - return store; + const store = writable(localStorage.getItem(key) ?? defaultValue); + store.subscribe((value) => localStorage.setItem(key, value)); + return store; } -export const address = writableStorage("address", PUBLIC_MUSIKQUAD_SERVER); -export const token = writableStorage("token", ""); +export const address = writableStorage('address', PUBLIC_MUSIKQUAD_SERVER); +export const token = writableStorage('token', ''); export function makeThumbnailUrl(id: number) { - if (id === 0) { - return null; - } - return `${scheme}://${get(address)}/thumbnail/${id}?token=${get(token)}`; + if (id === 0) { + return null; + } + return `${scheme}://${get(address)}/thumbnail/${id}?token=${get(token)}`; } export function makeAudioUrl(id: TrackId) { - return `${scheme}://${get(address)}/audio/external_id/${id}?token=${get(token)}`; + return `${scheme}://${get(address)}/audio/external_id/${id}?token=${get(token)}`; } export function makeGenScopedTokenUrl(id: TrackId) { - return `${scheme}://${get(address)}/share/generate/${id}?token=${get(token)}`; -} - -export function makeShareUrl(token: string) { - return `${scheme}://${PUBLIC_BASEURL}/share/${token}`; + return `${scheme}://${get(address)}/share/generate/${id}?token=${get(token)}`; } export const currentTrack = writable(null); -export function getCurrentTrack(tracks: Map, queue: TrackId[], position: number | null): TrackWithId | null { - if (position === null) { - return null; - } - const id = queue.at(position); - if (id === undefined) { - return null; - } - const track = tracks.get(id); - if (track === undefined) { - return null; - } - return { - track, - id, - }; +export function getCurrentTrack( + tracks: Map, + queue: TrackId[], + position: number | null +): TrackWithId | null { + if (position === null) { + return null; + } + const id = queue.at(position); + if (id === undefined) { + return null; + } + const track = tracks.get(id); + if (track === undefined) { + return null; + } + return { + track, + id + }; } export const queuePosition = writable(null); @@ -58,50 +58,53 @@ export const tracks = writable>(new Map()); export const tracksSorted = writable([]); queuePosition.subscribe((pos) => currentTrack.set(getCurrentTrack(get(tracks), get(queue), pos))); -tracks.subscribe((newTracks) => currentTrack.set(getCurrentTrack(newTracks, get(queue), get(queuePosition)))); +tracks.subscribe((newTracks) => + currentTrack.set(getCurrentTrack(newTracks, get(queue), get(queuePosition))) +); export function setQueuePositionTo(track_id: TrackId) { - let q = get(queue); - const position = q.indexOf(track_id); - if (position !== -1) { - queuePosition.set(position); - } else { - q.push(track_id); - queue.set(q); - queuePosition.set(q.length - 1); - } + let q = get(queue); + const position = q.indexOf(track_id); + if (position !== -1) { + queuePosition.set(position); + } else { + q.push(track_id); + queue.set(q); + queuePosition.set(q.length - 1); + } } export function getPrevQueuePosition(respectLoop: boolean) { - const pos = get(queuePosition); - if (pos !== null) { - const q = get(queue); - const l = get(loop); - const _newPos = pos - 1; - const newPos = _newPos > -1 ? _newPos : l === LoopKind.Once || !respectLoop ? q.length - 1 : null; - return newPos; - } - return null; + const pos = get(queuePosition); + if (pos !== null) { + const q = get(queue); + const l = get(loop); + const _newPos = pos - 1; + const newPos = + _newPos > -1 ? _newPos : l === LoopKind.Once || !respectLoop ? q.length - 1 : null; + return newPos; + } + return null; } export function getNextQueuePosition(respectLoop: boolean) { - const pos = get(queuePosition); - if (pos !== null) { - const q = get(queue); - const l = get(loop); - const _newPos = pos + 1; - const newPos = _newPos < q.length ? _newPos : l === LoopKind.Once || !respectLoop ? 0 : null; - return newPos; - } - return null; + const pos = get(queuePosition); + if (pos !== null) { + const q = get(queue); + const l = get(loop); + const _newPos = pos + 1; + const newPos = _newPos < q.length ? _newPos : l === LoopKind.Once || !respectLoop ? 0 : null; + return newPos; + } + return null; } export function prevQueuePosition(respectLoop: boolean = false) { - queuePosition.set(getPrevQueuePosition(respectLoop)); + queuePosition.set(getPrevQueuePosition(respectLoop)); } export function nextQueuePosition(respectLoop: boolean = false) { - queuePosition.set(getNextQueuePosition(respectLoop)); + queuePosition.set(getNextQueuePosition(respectLoop)); } export const paused = writable(false); @@ -110,51 +113,51 @@ export const muted = writable(false); export const loop = writable(LoopKind.Off); export function changeLoop() { - switch (get(loop)) { - case LoopKind.Always: - loop.set(LoopKind.Off); - break; - case LoopKind.Off: - loop.set(LoopKind.Once); - break; - case LoopKind.Once: - loop.set(LoopKind.Always); - break; - } + switch (get(loop)) { + case LoopKind.Always: + loop.set(LoopKind.Off); + break; + case LoopKind.Off: + loop.set(LoopKind.Once); + break; + case LoopKind.Once: + loop.set(LoopKind.Always); + break; + } } -export const searchText = writable(""); +export const searchText = writable(''); export function search(q: string) { - const query = q.trim(); - const t = get(tracks); + const query = q.trim(); + const t = get(tracks); - if (query.length === 0) { - let result: TrackId[] = []; - t.forEach((_, id) => (result.push(id))); - tracksSorted.set(result); - return; - } + if (query.length === 0) { + let result: TrackId[] = []; + t.forEach((_, id) => result.push(id)); + tracksSorted.set(result); + return; + } - const smartCase = query.toLowerCase() === query; + const smartCase = query.toLowerCase() === query; - let result: TrackId[] = []; - t.forEach((track, id) => { - if (smartCase) { - const titleHas = track.title.toLowerCase().includes(query); - const albumHas = track.album_title.toLowerCase().includes(query); - const artistHas = track.artist_name.toLowerCase().includes(query); - if (titleHas || albumHas || artistHas) { - result.push(id); - } - } else { - const titleHas = track.title.includes(query); - const albumHas = track.album_title.includes(query); - const artistHas = track.artist_name.includes(query); - if (titleHas || albumHas || artistHas) { - result.push(id); - } - } - }); - tracksSorted.set(result); -} \ No newline at end of file + let result: TrackId[] = []; + t.forEach((track, id) => { + if (smartCase) { + const titleHas = track.title.toLowerCase().includes(query); + const albumHas = track.album_title.toLowerCase().includes(query); + const artistHas = track.artist_name.toLowerCase().includes(query); + if (titleHas || albumHas || artistHas) { + result.push(id); + } + } else { + const titleHas = track.title.includes(query); + const albumHas = track.album_title.includes(query); + const artistHas = track.artist_name.includes(query); + if (titleHas || albumHas || artistHas) { + result.push(id); + } + } + }); + tracksSorted.set(result); +} diff --git a/src/types.ts b/src/types.ts index 83dc18e..f4c113a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,32 +2,32 @@ export type ResourceId = bigint; export type TrackId = string; export interface Track { - id: number, - title: string, - track_num: number, - album_title: string, - album_id: ResourceId, - artist_name: string, - artist_id: ResourceId, - thumbnail_id: number, + id: number; + title: string; + track_num: number; + album_title: string; + album_id: ResourceId; + artist_name: string; + artist_id: ResourceId; + thumbnail_id: number; } export interface TrackWithId { - id: TrackId, - track: Track, + id: TrackId; + track: Track; } export interface Artist { - name: string, + name: string; } export interface Album { - title: string, - artist_id: ResourceId, + title: string; + artist_id: ResourceId; } export enum LoopKind { - Off, - Once, - Always, -} \ No newline at end of file + Off, + Once, + Always +} diff --git a/src/utils.ts b/src/utils.ts index 0463bd6..86b876f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,67 +1,80 @@ import { dev } from '$app/environment'; +import { PUBLIC_BASEURL } from '$env/static/public'; -export const scheme = dev ? "http" : "https"; +export const scheme = dev ? 'http' : 'https'; + +export function makeShareUrl(token: string) { + return `${PUBLIC_BASEURL}/share/${token}`; +} export function getAudioElement() { - const elem = document.getElementById('audio-source'); - if (elem === null) { - return null; - } - return elem as HTMLAudioElement; + const elem = document.getElementById('audio-source'); + if (elem === null) { + return null; + } + return elem as HTMLAudioElement; } export function calculateMinuteSecond(seconds: number) { - let secs = Math.floor(seconds); - let secsLeftover = secs % 60; - let minutes = (secs - secsLeftover) / 60; + let secs = Math.floor(seconds); + let secsLeftover = secs % 60; + let minutes = (secs - secsLeftover) / 60; - let secondsFormatted = secsLeftover < 10 ? `0${secsLeftover}` : `${secsLeftover}`; - let minutesFormatted = minutes < 10 ? `0${minutes}` : `${minutes}`; + let secondsFormatted = secsLeftover < 10 ? `0${secsLeftover}` : `${secsLeftover}`; + let minutesFormatted = minutes < 10 ? `0${minutes}` : `${minutes}`; - return `${minutesFormatted}:${secondsFormatted}`; + return `${minutesFormatted}:${secondsFormatted}`; } -export function interceptKeys(extraActions: [string, () => void][] = []): (event: KeyboardEvent) => void { - return (event) => { - const tagName = document.activeElement?.tagName ?? ''; - const audio = getAudioElement(); - const actions = new Map([ - ...extraActions, - ['Space', () => { - if (audio !== null) { - audio.paused ? audio.play() : audio.pause(); - } - }], - ['KeyM', () => { - if (audio !== null) { - audio.muted = !audio.muted; - } - }], - [ - 'ArrowLeft', - () => { - if (audio !== null) { - audio.currentTime -= 5; - } - } - ], - [ - 'ArrowRight', - () => { - const audio = getAudioElement(); - if (audio !== null) { - audio.currentTime += 5; - } - } - ] - ]); - if (tagName !== 'INPUT' && actions.has(event.code)) { - event.preventDefault(); - event.stopPropagation(); - const action = actions.get(event.code) ?? null; - if (action !== null) { - action(); - } - } - } -} \ No newline at end of file +export function interceptKeys( + extraActions: [string, () => void][] = [] +): (event: KeyboardEvent) => void { + return (event) => { + const tagName = document.activeElement?.tagName ?? ''; + const audio = getAudioElement(); + const actions = new Map([ + ...extraActions, + [ + 'Space', + () => { + if (audio !== null) { + audio.paused ? audio.play() : audio.pause(); + } + } + ], + [ + 'KeyM', + () => { + if (audio !== null) { + audio.muted = !audio.muted; + } + } + ], + [ + 'ArrowLeft', + () => { + if (audio !== null) { + audio.currentTime -= 5; + } + } + ], + [ + 'ArrowRight', + () => { + const audio = getAudioElement(); + if (audio !== null) { + audio.currentTime += 5; + } + } + ] + ]); + if (tagName !== 'INPUT' && actions.has(event.code)) { + event.preventDefault(); + event.stopPropagation(); + const action = actions.get(event.code) ?? null; + if (action !== null) { + action(); + } + } + }; +} diff --git a/tailwind.config.js b/tailwind.config.js index 0380caa..aefbbfc 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,17 +1,12 @@ /** @type {import('tailwindcss').Config} */ export default { - content: [ - './src/**/*.{html,js,svelte,ts}', - require('path').join(require.resolve( - '@skeletonlabs/skeleton'), - '../**/*.{html,js,svelte,ts}' - ), - ], - theme: { - extend: {}, - }, - plugins: [ - ...require('@skeletonlabs/skeleton/tailwind/skeleton.cjs')() - ], - darkMode: 'class', -} \ No newline at end of file + content: [ + './src/**/*.{html,js,svelte,ts}', + require('path').join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}') + ], + theme: { + extend: {} + }, + plugins: [...require('@skeletonlabs/skeleton/tailwind/skeleton.cjs')()], + darkMode: 'class' +}; diff --git a/vite.config.ts b/vite.config.ts index 3af8dd8..8911bd4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,12 +1,12 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; -import Icons from 'unplugin-icons/vite' +import Icons from 'unplugin-icons/vite'; export default defineConfig({ plugins: [ sveltekit(), Icons({ - compiler: 'svelte', + compiler: 'svelte' }) ] });