fix: dont copy scheme in share
This commit is contained in:
parent
9a0d05df94
commit
54ddf32e21
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"svelte.enable-ts-plugin": true,
|
"svelte.enable-ts-plugin": true,
|
||||||
"editor.tabSize": 2
|
"editor.tabSize": 2
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {}
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
|
2
src/app.d.ts
vendored
2
src/app.d.ts
vendored
@ -11,4 +11,4 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { };
|
export {};
|
||||||
|
20
src/app.html
20
src/app.html
@ -1,15 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html class="dark" lang="en">
|
<html class="dark" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body data-sveltekit-preload-data="hover" data-theme="crimson">
|
||||||
<meta charset="utf-8" />
|
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
</body>
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
%sveltekit.head%
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover" data-theme="crimson">
|
|
||||||
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -1,2 +1,7 @@
|
|||||||
html, body { @apply h-full overflow-hidden; }
|
html,
|
||||||
.card { @apply shadow shadow-black; }
|
body {
|
||||||
|
@apply h-full overflow-hidden;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
@apply shadow shadow-black;
|
||||||
|
}
|
||||||
|
296
src/comms.ts
296
src/comms.ts
@ -1,174 +1,176 @@
|
|||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import type { TrackWithId } from "./types";
|
import type { TrackWithId } from './types';
|
||||||
|
|
||||||
const API_VERSION: number = 20;
|
const API_VERSION: number = 20;
|
||||||
const HTTP_DISABLED_ERROR: string =
|
const HTTP_DISABLED_ERROR: string =
|
||||||
"server does not have HTTP resources enabled, you will not be able to stream music";
|
'server does not have HTTP resources enabled, you will not be able to stream music';
|
||||||
const SERVER_API_INCOMPATIBLE_ERROR: (serverApi: number) => string =
|
const SERVER_API_INCOMPATIBLE_ERROR: (serverApi: number) => string = (serverApi) =>
|
||||||
(serverApi) => `server API version (${serverApi}) is different from our supported version (${API_VERSION})`;
|
`server API version (${serverApi}) is different from our supported version (${API_VERSION})`;
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
device_id: string;
|
device_id: string;
|
||||||
type: MessageType;
|
type: MessageType;
|
||||||
options: any;
|
options: any;
|
||||||
};
|
}
|
||||||
type MessageType = 'request' | 'response' | 'broadcast';
|
type MessageType = 'request' | 'response' | 'broadcast';
|
||||||
type RequestCallback = (arg0: Message | null) => void;
|
type RequestCallback = (arg0: Message | null) => void;
|
||||||
|
|
||||||
interface Callbacks {
|
interface Callbacks {
|
||||||
onDisconnect: (authenticated: boolean, reason: string) => void;
|
onDisconnect: (authenticated: boolean, reason: string) => void;
|
||||||
onConnect: (initial: Message) => void;
|
onConnect: (initial: Message) => void;
|
||||||
onIncompatible: (reason: string) => void;
|
onIncompatible: (reason: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MetadataCommunicator {
|
export class MetadataCommunicator {
|
||||||
ws: WebSocket | null;
|
ws: WebSocket | null;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
callbacks: Map<string, RequestCallback>;
|
callbacks: Map<string, RequestCallback>;
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
eventCallbacks: Callbacks;
|
eventCallbacks: Callbacks;
|
||||||
onConnectCallbacks: (() => void)[];
|
onConnectCallbacks: (() => void)[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.callbacks = new Map();
|
this.callbacks = new Map();
|
||||||
this.deviceId = crypto.randomUUID();
|
this.deviceId = crypto.randomUUID();
|
||||||
this.authenticated = false;
|
this.authenticated = false;
|
||||||
this.eventCallbacks = {
|
this.eventCallbacks = {
|
||||||
onDisconnect: () => { },
|
onDisconnect: () => {},
|
||||||
onConnect: () => { },
|
onConnect: () => {},
|
||||||
onIncompatible: () => { },
|
onIncompatible: () => {}
|
||||||
};
|
};
|
||||||
this.onConnectCallbacks = [];
|
this.onConnectCallbacks = [];
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCallbacks(callbacks: Callbacks) {
|
setCallbacks(callbacks: Callbacks) {
|
||||||
this.eventCallbacks = callbacks;
|
this.eventCallbacks = callbacks;
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(address: string, password: string) {
|
connect(address: string, password: string) {
|
||||||
this.close();
|
this.close();
|
||||||
|
|
||||||
const scheme = dev ? "ws" : "wss";
|
const scheme = dev ? 'ws' : 'wss';
|
||||||
this.ws = new WebSocket(`${scheme}://${address}`);
|
this.ws = new WebSocket(`${scheme}://${address}`);
|
||||||
|
|
||||||
this.ws.addEventListener('open', (event) => {
|
this.ws.addEventListener('open', (event) => {
|
||||||
this.makeRequest("authenticate", 'request', { password }, (msg) => {
|
this.makeRequest('authenticate', 'request', { password }, (msg) => {
|
||||||
if (msg!.options.authenticated) {
|
if (msg!.options.authenticated) {
|
||||||
this.authenticated = true;
|
this.authenticated = true;
|
||||||
this.eventCallbacks.onConnect(msg!);
|
this.eventCallbacks.onConnect(msg!);
|
||||||
this.onConnectCallbacks.forEach((f) => f());
|
this.onConnectCallbacks.forEach((f) => f());
|
||||||
this.onConnectCallbacks = [];
|
this.onConnectCallbacks = [];
|
||||||
if (!msg!.options.environment.http_server_enabled) {
|
if (!msg!.options.environment.http_server_enabled) {
|
||||||
this.eventCallbacks.onIncompatible(HTTP_DISABLED_ERROR);
|
this.eventCallbacks.onIncompatible(HTTP_DISABLED_ERROR);
|
||||||
}
|
}
|
||||||
const serverApiVersion = msg!.options.environment.api_version;
|
const serverApiVersion = msg!.options.environment.api_version;
|
||||||
if (serverApiVersion != API_VERSION) {
|
if (serverApiVersion != API_VERSION) {
|
||||||
this.eventCallbacks.onIncompatible(SERVER_API_INCOMPATIBLE_ERROR(serverApiVersion));
|
this.eventCallbacks.onIncompatible(SERVER_API_INCOMPATIBLE_ERROR(serverApiVersion));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.ws.addEventListener('close', (event) => {
|
this.ws.addEventListener('close', (event) => {
|
||||||
this.eventCallbacks.onDisconnect(this.authenticated, `${event.reason} (code ${event.code})`);
|
this.eventCallbacks.onDisconnect(this.authenticated, `${event.reason} (code ${event.code})`);
|
||||||
this.authenticated = false;
|
this.authenticated = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ws.addEventListener('message', (event) => {
|
this.ws.addEventListener('message', (event) => {
|
||||||
const parsed: Message = JSON.parse(event.data);
|
const parsed: Message = JSON.parse(event.data);
|
||||||
const maybeCallback = this.callbacks.get(parsed.id);
|
const maybeCallback = this.callbacks.get(parsed.id);
|
||||||
if (maybeCallback) {
|
if (maybeCallback) {
|
||||||
maybeCallback(parsed);
|
maybeCallback(parsed);
|
||||||
this.callbacks.delete(parsed.id);
|
this.callbacks.delete(parsed.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTracksCount(): Promise<number> {
|
fetchTracksCount(): Promise<number> {
|
||||||
const options = { count_only: true };
|
const options = { count_only: true };
|
||||||
const th = this;
|
const th = this;
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
th.makeRequest("query_tracks", "request", options, (resp) => {
|
th.makeRequest('query_tracks', 'request', options, (resp) => {
|
||||||
if (resp) {
|
if (resp) {
|
||||||
resolve(resp.options.count);
|
resolve(resp.options.count);
|
||||||
} else {
|
} else {
|
||||||
reject(null);
|
reject(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTracks(limit: number, offset: number, filter: string | null = null): Promise<TrackWithId[]> {
|
fetchTracks(limit: number, offset: number, filter: string | null = null): Promise<TrackWithId[]> {
|
||||||
const options: any = { limit, offset };
|
const options: any = { limit, offset };
|
||||||
if (filter !== null) options.filter = filter;
|
if (filter !== null) options.filter = filter;
|
||||||
|
|
||||||
const th = this;
|
const th = this;
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
th.makeRequest("query_tracks", "request", options, (resp) => {
|
th.makeRequest('query_tracks', 'request', options, (resp) => {
|
||||||
if (resp) {
|
if (resp) {
|
||||||
const data: any[] = resp.options.data;
|
const data: any[] = resp.options.data;
|
||||||
resolve(data.map((t) => ({
|
resolve(
|
||||||
id: t.external_id,
|
data.map((t) => ({
|
||||||
track: {
|
id: t.external_id,
|
||||||
id: t.id,
|
track: {
|
||||||
title: t.title,
|
id: t.id,
|
||||||
track_num: t.track,
|
title: t.title,
|
||||||
album_title: t.album,
|
track_num: t.track,
|
||||||
album_id: t.album_id,
|
album_title: t.album,
|
||||||
artist_name: t.artist,
|
album_id: t.album_id,
|
||||||
artist_id: t.artist_id,
|
artist_name: t.artist,
|
||||||
thumbnail_id: t.thumbnail_id,
|
artist_id: t.artist_id,
|
||||||
}
|
thumbnail_id: t.thumbnail_id
|
||||||
})));
|
}
|
||||||
} else {
|
}))
|
||||||
reject(null);
|
);
|
||||||
}
|
} else {
|
||||||
});
|
reject(null);
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private makeRequest(name: string, type: MessageType, options: object, callback: RequestCallback) {
|
private makeRequest(name: string, type: MessageType, options: object, callback: RequestCallback) {
|
||||||
// return if not authenticated, allow authentication messages
|
// return if not authenticated, allow authentication messages
|
||||||
if (this.isClosed() || !this.authenticated && name != "authenticate") {
|
if (this.isClosed() || (!this.authenticated && name != 'authenticate')) {
|
||||||
callback(null);
|
callback(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Unique enough for our purposes (as request ID)
|
// Unique enough for our purposes (as request ID)
|
||||||
const id = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
const id = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||||
this.callbacks.set(id, callback);
|
this.callbacks.set(id, callback);
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
options,
|
options,
|
||||||
device_id: this.deviceId,
|
device_id: this.deviceId,
|
||||||
id,
|
id
|
||||||
});
|
});
|
||||||
console.trace("sending metadata message: " + payload);
|
console.trace('sending metadata message: ' + payload);
|
||||||
this.ws!.send(payload);
|
this.ws!.send(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
if (this.isClosed()) return;
|
if (this.isClosed()) return;
|
||||||
this.ws!.close();
|
this.ws!.close();
|
||||||
this.authenticated = false;
|
this.authenticated = false;
|
||||||
this.callbacks.clear();
|
this.callbacks.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
isClosed() {
|
isClosed() {
|
||||||
return (
|
return (
|
||||||
this.ws === null
|
this.ws === null ||
|
||||||
|| this.ws.readyState === WebSocket.CLOSED
|
this.ws.readyState === WebSocket.CLOSED ||
|
||||||
|| this.ws.readyState === WebSocket.CLOSING
|
this.ws.readyState === WebSocket.CLOSING
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onConnect(cb: () => Promise<void>) {
|
onConnect(cb: () => Promise<void>) {
|
||||||
if (!this.isClosed() && this.authenticated) {
|
if (!this.isClosed() && this.authenticated) {
|
||||||
cb();
|
cb();
|
||||||
} else {
|
} else {
|
||||||
this.onConnectCallbacks = [...this.onConnectCallbacks, cb];
|
this.onConnectCallbacks = [...this.onConnectCallbacks, cb];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,14 +4,13 @@
|
|||||||
makeThumbnailUrl,
|
makeThumbnailUrl,
|
||||||
currentTrack,
|
currentTrack,
|
||||||
setQueuePositionTo,
|
setQueuePositionTo,
|
||||||
makeGenScopedTokenUrl,
|
makeGenScopedTokenUrl
|
||||||
makeShareUrl
|
|
||||||
} from '../stores';
|
} from '../stores';
|
||||||
import Spinnny from '~icons/line-md/loading-loop';
|
import Spinnny from '~icons/line-md/loading-loop';
|
||||||
import IconPlay from '~icons/mdi/play';
|
import IconPlay from '~icons/mdi/play';
|
||||||
import IconMusic from '~icons/mdi/music';
|
import IconMusic from '~icons/mdi/music';
|
||||||
import { toastStore } from '@skeletonlabs/skeleton';
|
import { toastStore } from '@skeletonlabs/skeleton';
|
||||||
import { getAudioElement } from '../utils';
|
import { getAudioElement, makeShareUrl } from '../utils';
|
||||||
|
|
||||||
export let track_with_id: TrackWithId;
|
export let track_with_id: TrackWithId;
|
||||||
let track = track_with_id.track;
|
let track = track_with_id.track;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { MetadataCommunicator } from "../../comms";
|
import { MetadataCommunicator } from '../../comms';
|
||||||
|
|
||||||
export const _metadataComm = new MetadataCommunicator();
|
export const _metadataComm = new MetadataCommunicator();
|
||||||
export const ssr = false;
|
export const ssr = false;
|
||||||
|
@ -3,17 +3,17 @@ import { LOCAL_MUSIKQUAD_SERVER } from '$env/static/private';
|
|||||||
import { scheme } from '../../../utils';
|
import { scheme } from '../../../utils';
|
||||||
|
|
||||||
interface MusicInfo {
|
interface MusicInfo {
|
||||||
title: string,
|
title: string;
|
||||||
album: string,
|
album: string;
|
||||||
artist: string,
|
artist: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function load({ params }) {
|
export async function load({ params }) {
|
||||||
const resp = await fetch(`${LOCAL_MUSIKQUAD_SERVER}/share/info/${params.token}`);
|
const resp = await fetch(`${LOCAL_MUSIKQUAD_SERVER}/share/info/${params.token}`);
|
||||||
const info: MusicInfo = await resp.json();
|
const info: MusicInfo = await resp.json();
|
||||||
return {
|
return {
|
||||||
info,
|
info,
|
||||||
thumbnail_url: `${scheme}://${PUBLIC_MUSIKQUAD_SERVER}/share/thumbnail/${params.token}`,
|
thumbnail_url: `${scheme}://${PUBLIC_MUSIKQUAD_SERVER}/share/thumbnail/${params.token}`,
|
||||||
audio_url: `${scheme}://${PUBLIC_MUSIKQUAD_SERVER}/share/audio/${params.token}`,
|
audio_url: `${scheme}://${PUBLIC_MUSIKQUAD_SERVER}/share/audio/${params.token}`
|
||||||
};
|
};
|
||||||
}
|
}
|
207
src/stores.ts
207
src/stores.ts
@ -1,55 +1,55 @@
|
|||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import { type Track, type TrackId, type TrackWithId, LoopKind } from './types';
|
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';
|
import { scheme } from './utils';
|
||||||
|
|
||||||
function writableStorage(key: string, defaultValue: string) {
|
function writableStorage(key: string, defaultValue: string) {
|
||||||
const store = writable(localStorage.getItem(key) ?? defaultValue);
|
const store = writable(localStorage.getItem(key) ?? defaultValue);
|
||||||
store.subscribe(value => localStorage.setItem(key, value));
|
store.subscribe((value) => localStorage.setItem(key, value));
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const address = writableStorage("address", PUBLIC_MUSIKQUAD_SERVER);
|
export const address = writableStorage('address', PUBLIC_MUSIKQUAD_SERVER);
|
||||||
export const token = writableStorage("token", "");
|
export const token = writableStorage('token', '');
|
||||||
|
|
||||||
export function makeThumbnailUrl(id: number) {
|
export function makeThumbnailUrl(id: number) {
|
||||||
if (id === 0) {
|
if (id === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return `${scheme}://${get(address)}/thumbnail/${id}?token=${get(token)}`;
|
return `${scheme}://${get(address)}/thumbnail/${id}?token=${get(token)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeAudioUrl(id: TrackId) {
|
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) {
|
export function makeGenScopedTokenUrl(id: TrackId) {
|
||||||
return `${scheme}://${get(address)}/share/generate/${id}?token=${get(token)}`;
|
return `${scheme}://${get(address)}/share/generate/${id}?token=${get(token)}`;
|
||||||
}
|
|
||||||
|
|
||||||
export function makeShareUrl(token: string) {
|
|
||||||
return `${scheme}://${PUBLIC_BASEURL}/share/${token}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const currentTrack = writable<TrackWithId | null>(null);
|
export const currentTrack = writable<TrackWithId | null>(null);
|
||||||
|
|
||||||
export function getCurrentTrack(tracks: Map<TrackId, Track>, queue: TrackId[], position: number | null): TrackWithId | null {
|
export function getCurrentTrack(
|
||||||
if (position === null) {
|
tracks: Map<TrackId, Track>,
|
||||||
return null;
|
queue: TrackId[],
|
||||||
}
|
position: number | null
|
||||||
const id = queue.at(position);
|
): TrackWithId | null {
|
||||||
if (id === undefined) {
|
if (position === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const track = tracks.get(id);
|
const id = queue.at(position);
|
||||||
if (track === undefined) {
|
if (id === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
const track = tracks.get(id);
|
||||||
track,
|
if (track === undefined) {
|
||||||
id,
|
return null;
|
||||||
};
|
}
|
||||||
|
return {
|
||||||
|
track,
|
||||||
|
id
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const queuePosition = writable<number | null>(null);
|
export const queuePosition = writable<number | null>(null);
|
||||||
@ -58,50 +58,53 @@ export const tracks = writable<Map<TrackId, Track>>(new Map());
|
|||||||
export const tracksSorted = writable<TrackId[]>([]);
|
export const tracksSorted = writable<TrackId[]>([]);
|
||||||
|
|
||||||
queuePosition.subscribe((pos) => currentTrack.set(getCurrentTrack(get(tracks), get(queue), pos)));
|
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) {
|
export function setQueuePositionTo(track_id: TrackId) {
|
||||||
let q = get(queue);
|
let q = get(queue);
|
||||||
const position = q.indexOf(track_id);
|
const position = q.indexOf(track_id);
|
||||||
if (position !== -1) {
|
if (position !== -1) {
|
||||||
queuePosition.set(position);
|
queuePosition.set(position);
|
||||||
} else {
|
} else {
|
||||||
q.push(track_id);
|
q.push(track_id);
|
||||||
queue.set(q);
|
queue.set(q);
|
||||||
queuePosition.set(q.length - 1);
|
queuePosition.set(q.length - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPrevQueuePosition(respectLoop: boolean) {
|
export function getPrevQueuePosition(respectLoop: boolean) {
|
||||||
const pos = get(queuePosition);
|
const pos = get(queuePosition);
|
||||||
if (pos !== null) {
|
if (pos !== null) {
|
||||||
const q = get(queue);
|
const q = get(queue);
|
||||||
const l = get(loop);
|
const l = get(loop);
|
||||||
const _newPos = pos - 1;
|
const _newPos = pos - 1;
|
||||||
const newPos = _newPos > -1 ? _newPos : l === LoopKind.Once || !respectLoop ? q.length - 1 : null;
|
const newPos =
|
||||||
return newPos;
|
_newPos > -1 ? _newPos : l === LoopKind.Once || !respectLoop ? q.length - 1 : null;
|
||||||
}
|
return newPos;
|
||||||
return null;
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNextQueuePosition(respectLoop: boolean) {
|
export function getNextQueuePosition(respectLoop: boolean) {
|
||||||
const pos = get(queuePosition);
|
const pos = get(queuePosition);
|
||||||
if (pos !== null) {
|
if (pos !== null) {
|
||||||
const q = get(queue);
|
const q = get(queue);
|
||||||
const l = get(loop);
|
const l = get(loop);
|
||||||
const _newPos = pos + 1;
|
const _newPos = pos + 1;
|
||||||
const newPos = _newPos < q.length ? _newPos : l === LoopKind.Once || !respectLoop ? 0 : null;
|
const newPos = _newPos < q.length ? _newPos : l === LoopKind.Once || !respectLoop ? 0 : null;
|
||||||
return newPos;
|
return newPos;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prevQueuePosition(respectLoop: boolean = false) {
|
export function prevQueuePosition(respectLoop: boolean = false) {
|
||||||
queuePosition.set(getPrevQueuePosition(respectLoop));
|
queuePosition.set(getPrevQueuePosition(respectLoop));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nextQueuePosition(respectLoop: boolean = false) {
|
export function nextQueuePosition(respectLoop: boolean = false) {
|
||||||
queuePosition.set(getNextQueuePosition(respectLoop));
|
queuePosition.set(getNextQueuePosition(respectLoop));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const paused = writable<boolean>(false);
|
export const paused = writable<boolean>(false);
|
||||||
@ -110,51 +113,51 @@ export const muted = writable<boolean>(false);
|
|||||||
export const loop = writable<LoopKind>(LoopKind.Off);
|
export const loop = writable<LoopKind>(LoopKind.Off);
|
||||||
|
|
||||||
export function changeLoop() {
|
export function changeLoop() {
|
||||||
switch (get(loop)) {
|
switch (get(loop)) {
|
||||||
case LoopKind.Always:
|
case LoopKind.Always:
|
||||||
loop.set(LoopKind.Off);
|
loop.set(LoopKind.Off);
|
||||||
break;
|
break;
|
||||||
case LoopKind.Off:
|
case LoopKind.Off:
|
||||||
loop.set(LoopKind.Once);
|
loop.set(LoopKind.Once);
|
||||||
break;
|
break;
|
||||||
case LoopKind.Once:
|
case LoopKind.Once:
|
||||||
loop.set(LoopKind.Always);
|
loop.set(LoopKind.Always);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const searchText = writable<string>("");
|
export const searchText = writable<string>('');
|
||||||
|
|
||||||
export function search(q: string) {
|
export function search(q: string) {
|
||||||
const query = q.trim();
|
const query = q.trim();
|
||||||
const t = get(tracks);
|
const t = get(tracks);
|
||||||
|
|
||||||
if (query.length === 0) {
|
if (query.length === 0) {
|
||||||
let result: TrackId[] = [];
|
let result: TrackId[] = [];
|
||||||
t.forEach((_, id) => (result.push(id)));
|
t.forEach((_, id) => result.push(id));
|
||||||
tracksSorted.set(result);
|
tracksSorted.set(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const smartCase = query.toLowerCase() === query;
|
const smartCase = query.toLowerCase() === query;
|
||||||
|
|
||||||
let result: TrackId[] = [];
|
let result: TrackId[] = [];
|
||||||
t.forEach((track, id) => {
|
t.forEach((track, id) => {
|
||||||
if (smartCase) {
|
if (smartCase) {
|
||||||
const titleHas = track.title.toLowerCase().includes(query);
|
const titleHas = track.title.toLowerCase().includes(query);
|
||||||
const albumHas = track.album_title.toLowerCase().includes(query);
|
const albumHas = track.album_title.toLowerCase().includes(query);
|
||||||
const artistHas = track.artist_name.toLowerCase().includes(query);
|
const artistHas = track.artist_name.toLowerCase().includes(query);
|
||||||
if (titleHas || albumHas || artistHas) {
|
if (titleHas || albumHas || artistHas) {
|
||||||
result.push(id);
|
result.push(id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const titleHas = track.title.includes(query);
|
const titleHas = track.title.includes(query);
|
||||||
const albumHas = track.album_title.includes(query);
|
const albumHas = track.album_title.includes(query);
|
||||||
const artistHas = track.artist_name.includes(query);
|
const artistHas = track.artist_name.includes(query);
|
||||||
if (titleHas || albumHas || artistHas) {
|
if (titleHas || albumHas || artistHas) {
|
||||||
result.push(id);
|
result.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tracksSorted.set(result);
|
tracksSorted.set(result);
|
||||||
}
|
}
|
32
src/types.ts
32
src/types.ts
@ -2,32 +2,32 @@ export type ResourceId = bigint;
|
|||||||
export type TrackId = string;
|
export type TrackId = string;
|
||||||
|
|
||||||
export interface Track {
|
export interface Track {
|
||||||
id: number,
|
id: number;
|
||||||
title: string,
|
title: string;
|
||||||
track_num: number,
|
track_num: number;
|
||||||
album_title: string,
|
album_title: string;
|
||||||
album_id: ResourceId,
|
album_id: ResourceId;
|
||||||
artist_name: string,
|
artist_name: string;
|
||||||
artist_id: ResourceId,
|
artist_id: ResourceId;
|
||||||
thumbnail_id: number,
|
thumbnail_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrackWithId {
|
export interface TrackWithId {
|
||||||
id: TrackId,
|
id: TrackId;
|
||||||
track: Track,
|
track: Track;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Artist {
|
export interface Artist {
|
||||||
name: string,
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Album {
|
export interface Album {
|
||||||
title: string,
|
title: string;
|
||||||
artist_id: ResourceId,
|
artist_id: ResourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LoopKind {
|
export enum LoopKind {
|
||||||
Off,
|
Off,
|
||||||
Once,
|
Once,
|
||||||
Always,
|
Always
|
||||||
}
|
}
|
123
src/utils.ts
123
src/utils.ts
@ -1,67 +1,80 @@
|
|||||||
import { dev } from '$app/environment';
|
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() {
|
export function getAudioElement() {
|
||||||
const elem = document.getElementById('audio-source');
|
const elem = document.getElementById('audio-source');
|
||||||
if (elem === null) {
|
if (elem === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return elem as HTMLAudioElement;
|
return elem as HTMLAudioElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateMinuteSecond(seconds: number) {
|
export function calculateMinuteSecond(seconds: number) {
|
||||||
let secs = Math.floor(seconds);
|
let secs = Math.floor(seconds);
|
||||||
let secsLeftover = secs % 60;
|
let secsLeftover = secs % 60;
|
||||||
let minutes = (secs - secsLeftover) / 60;
|
let minutes = (secs - secsLeftover) / 60;
|
||||||
|
|
||||||
let secondsFormatted = secsLeftover < 10 ? `0${secsLeftover}` : `${secsLeftover}`;
|
let secondsFormatted = secsLeftover < 10 ? `0${secsLeftover}` : `${secsLeftover}`;
|
||||||
let minutesFormatted = minutes < 10 ? `0${minutes}` : `${minutes}`;
|
let minutesFormatted = minutes < 10 ? `0${minutes}` : `${minutes}`;
|
||||||
|
|
||||||
return `${minutesFormatted}:${secondsFormatted}`;
|
return `${minutesFormatted}:${secondsFormatted}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function interceptKeys(extraActions: [string, () => void][] = []): (event: KeyboardEvent) => void {
|
export function interceptKeys(
|
||||||
return (event) => {
|
extraActions: [string, () => void][] = []
|
||||||
const tagName = document.activeElement?.tagName ?? '';
|
): (event: KeyboardEvent) => void {
|
||||||
const audio = getAudioElement();
|
return (event) => {
|
||||||
const actions = new Map([
|
const tagName = document.activeElement?.tagName ?? '';
|
||||||
...extraActions,
|
const audio = getAudioElement();
|
||||||
['Space', () => {
|
const actions = new Map([
|
||||||
if (audio !== null) {
|
...extraActions,
|
||||||
audio.paused ? audio.play() : audio.pause();
|
[
|
||||||
}
|
'Space',
|
||||||
}],
|
() => {
|
||||||
['KeyM', () => {
|
if (audio !== null) {
|
||||||
if (audio !== null) {
|
audio.paused ? audio.play() : audio.pause();
|
||||||
audio.muted = !audio.muted;
|
}
|
||||||
}
|
}
|
||||||
}],
|
],
|
||||||
[
|
[
|
||||||
'ArrowLeft',
|
'KeyM',
|
||||||
() => {
|
() => {
|
||||||
if (audio !== null) {
|
if (audio !== null) {
|
||||||
audio.currentTime -= 5;
|
audio.muted = !audio.muted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'ArrowRight',
|
'ArrowLeft',
|
||||||
() => {
|
() => {
|
||||||
const audio = getAudioElement();
|
if (audio !== null) {
|
||||||
if (audio !== null) {
|
audio.currentTime -= 5;
|
||||||
audio.currentTime += 5;
|
}
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
]
|
[
|
||||||
]);
|
'ArrowRight',
|
||||||
if (tagName !== 'INPUT' && actions.has(event.code)) {
|
() => {
|
||||||
event.preventDefault();
|
const audio = getAudioElement();
|
||||||
event.stopPropagation();
|
if (audio !== null) {
|
||||||
const action = actions.get(event.code) ?? null;
|
audio.currentTime += 5;
|
||||||
if (action !== null) {
|
}
|
||||||
action();
|
}
|
||||||
}
|
]
|
||||||
}
|
]);
|
||||||
}
|
if (tagName !== 'INPUT' && actions.has(event.code)) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const action = actions.get(event.code) ?? null;
|
||||||
|
if (action !== null) {
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
@ -1,17 +1,12 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
'./src/**/*.{html,js,svelte,ts}',
|
'./src/**/*.{html,js,svelte,ts}',
|
||||||
require('path').join(require.resolve(
|
require('path').join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')
|
||||||
'@skeletonlabs/skeleton'),
|
],
|
||||||
'../**/*.{html,js,svelte,ts}'
|
theme: {
|
||||||
),
|
extend: {}
|
||||||
],
|
},
|
||||||
theme: {
|
plugins: [...require('@skeletonlabs/skeleton/tailwind/skeleton.cjs')()],
|
||||||
extend: {},
|
darkMode: 'class'
|
||||||
},
|
};
|
||||||
plugins: [
|
|
||||||
...require('@skeletonlabs/skeleton/tailwind/skeleton.cjs')()
|
|
||||||
],
|
|
||||||
darkMode: 'class',
|
|
||||||
}
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import Icons from 'unplugin-icons/vite'
|
import Icons from 'unplugin-icons/vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
sveltekit(),
|
sveltekit(),
|
||||||
Icons({
|
Icons({
|
||||||
compiler: 'svelte',
|
compiler: 'svelte'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user