7 Commits
20 changed files with 828 additions and 47 deletions
+5 -2
View File
@@ -11,6 +11,9 @@
"type-check": "vue-tsc --build" "type-check": "vue-tsc --build"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.3.1",
"@fortawesome/fontawesome-svg-core": "^7.3.1",
"@fortawesome/free-regular-svg-icons": "^7.3.1",
"pinia": "^4.0.2", "pinia": "^4.0.2",
"vue": "^3.5.40", "vue": "^3.5.40",
"vue-router": "^5.2.0" "vue-router": "^5.2.0"
@@ -24,14 +27,14 @@
"@vue/tsconfig": "^0.9.1", "@vue/tsconfig": "^0.9.1",
"eslint-plugin-vue": "^10.10.0", "eslint-plugin-vue": "^10.10.0",
"globals": "^17.11.0", "globals": "^17.11.0",
"musickitjs-v3-types": "^1.0.0",
"npm-run-all2": "^9.0.2", "npm-run-all2": "^9.0.2",
"sass-embedded": "^1.103.1", "sass-embedded": "^1.103.1",
"typescript": "~6.0.0", "typescript": "~6.0.0",
"typescript-eslint": "^8.67.0", "typescript-eslint": "^8.67.0",
"vite": "^8.1.5", "vite": "^8.1.5",
"vite-plugin-vue-devtools": "^8.1.5", "vite-plugin-vue-devtools": "^8.1.5",
"vue-tsc": "^3.3.7", "vue-tsc": "^3.3.7"
"musickitjs-v3-types": "^1.0.0"
}, },
"engines": { "engines": {
"node": "^22.18.0 || >=24.12.0" "node": "^22.18.0 || >=24.12.0"
+3 -3
View File
@@ -57,7 +57,7 @@
--secondary-color: white; --secondary-color: white;
--background-color: rgb(221, 221, 221); --background-color: rgb(221, 221, 221);
--nav-background: white; --nav-background: white;
--hover-color: #00457a; --accent-color: #00457a;
--popup-color: rgb(224, 224, 224); --popup-color: rgb(224, 224, 224);
--overlay-color: rgba(0, 0, 0, 0.7); --overlay-color: rgba(0, 0, 0, 0.7);
--PI: 3.14159265358979; --PI: 3.14159265358979;
@@ -74,7 +74,7 @@
--background-color: rgb(32, 32, 32); --background-color: rgb(32, 32, 32);
--nav-background: rgb(54, 54, 54); --nav-background: rgb(54, 54, 54);
--popup-color: rgb(58, 58, 58); --popup-color: rgb(58, 58, 58);
--hover-color: #007ddd; --accent-color: #007ddd;
--overlay-color: rgba(104, 104, 104, 0.575); --overlay-color: rgba(104, 104, 104, 0.575);
--gray-color: rgb(207, 207, 207); --gray-color: rgb(207, 207, 207);
--footer-background: rgb(53, 53, 53); --footer-background: rgb(53, 53, 53);
@@ -90,7 +90,7 @@
--background-color: rgb(32, 32, 32); --background-color: rgb(32, 32, 32);
--nav-background: rgb(54, 54, 54); --nav-background: rgb(54, 54, 54);
--popup-color: rgb(58, 58, 58); --popup-color: rgb(58, 58, 58);
--hover-color: #007ddd; --accent-color: #007ddd;
--overlay-color: rgba(104, 104, 104, 0.575); --overlay-color: rgba(104, 104, 104, 0.575);
--gray-color: rgb(207, 207, 207); --gray-color: rgb(207, 207, 207);
--footer-background: rgb(53, 53, 53); --footer-background: rgb(53, 53, 53);
+83
View File
@@ -0,0 +1,83 @@
<script setup lang="ts">
import ProgressBar from './ProgressBar.vue';
import {
beautifyTime
} from '@/ts/util/time';
import player from '@/ts/player';
const playbackPercentage = player.playbackPercentage;
const duration = player.duration;
const repeatMode = player.repeat;
const shuffleMode = player.shuffle;
const seek = () => {
player.seekTo( playbackPercentage.value );
};
const toggleRepeatMode = () => {
switch ( repeatMode.value ) {
case 'one':
player.setRepeat( 'all' );
break;
case 'off':
player.setRepeat( 'one' );
break;
case 'all':
player.setRepeat( 'off' );
}
};
const openShareMenu = () => {
alert( 'Share menu not yet implemented' );
};
// TODO: Button availability
</script>
<template>
<div class="mp-player">
<div class="controls">
<i class="fa-solid fa-backward-step" @click="player.prev"></i>
<i class="fa-solid fa-arrow-rotate-left quick-seek" @click="player.back10"></i>
<div :class="['play-pause', 'paused']">
<i class="fa-solid fa-play" @click="player.play"></i>
<i class="fa-solid fa-pause" @click="player.pause"></i>
</div>
<i class="fa-solid fa-arrow-rotate-right quick-seek" @click="player.skip10"></i>
<i class="fa-solid fa-forward-step" @click="player.next"></i>
</div>
<div class="time">
<!-- TODO: Probably needs to be a computed -->
<p class="current">
{{ beautifyTime( playbackPercentage * duration ) }}
</p>
<p class="duration">
{{ beautifyTime( duration ) }}
</p>
</div>
<ProgressBar v-model="playbackPercentage" @move-end="seek" />
<div class="bottom-bar">
<!-- FA being a POS means need to make own mods to it -->
<i
:class="[
'fa-solid',
'fa-repeat',
'repeat',
repeatMode === 'one' ? 'once' : undefined,
repeatMode === 'all' ? 'all' : undefined
]"
@click="toggleRepeatMode"
></i>
<i class="fa-solid fa-share-from-square" @click="openShareMenu"></i>
<i
:class="['fa-solid', 'fa-shuffle', shuffleMode ? 'active' : undefined]"
@click="player.setShuffle( !shuffleMode )"
></i>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '@/scss/components/player.scss';
</style>
+5
View File
@@ -0,0 +1,5 @@
<template>
<div>
Hello World
</div>
</template>
+22 -22
View File
@@ -22,7 +22,7 @@
</script> </script>
<style scoped> <style scoped lang="scss">
.popup-backdrop { .popup-backdrop {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@@ -37,29 +37,29 @@
transition: all 0.5s; transition: all 0.5s;
transform: scale(1); transform: scale(1);
z-index: 99; z-index: 99;
}
.hidden { &.hidden {
transform: scale(0); transform: scale(0);
} }
.popup-main { >.popup-main {
width: 40%; width: 40%;
height: 50%; height: 50%;
background-color: var( --secondary-color ); background-color: var( --secondary-color );
padding: 2.5%; padding: 2.5%;
border-radius: 20px; border-radius: 20px;
position: relative; position: relative;
overflow-y: scroll; overflow-y: scroll;
display: block; display: block;
}
.close-icon { >.close-icon {
position: absolute; position: absolute;
top: 20px; top: 20px;
right: 20px; right: 20px;
font-size: 2rem; font-size: 2rem;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
}
}
} }
</style> </style>
+54
View File
@@ -0,0 +1,54 @@
<script setup lang="ts">
import {
ref,
useTemplateRef
} from 'vue';
const val = defineModel<number>( {
'required': true,
'default': 0.5
} );
const offset = ref( -1 );
const bar = useTemplateRef( 'bar' );
const start = ( ev: MouseEvent ) => {
offset.value = ev.x - bar.value!.offsetLeft;
val.value = offset.value / bar.value!.clientWidth;
emit( 'move-start' );
};
const move = ( ev: MouseEvent ) => {
if ( offset.value > -1 ) {
val.value = Math.max( 0, Math.min( ev.x / bar.value!.clientWidth, 1 ) );
}
};
const end = () => {
offset.value = -1;
emit( 'move-end' );
};
const emit = defineEmits<{
( e: 'move-end' ): void;
( e: 'move-start' ): void;
}>();
</script>
<template>
<div class="progressbar">
<div ref="bar" class="back">
<div :style="`width: ${ val * 100 }%;`"></div>
</div>
<div
:class="['click-target', offset >= 0 ? 'active' : undefined]"
@mousedown="start"
@mousemove="move"
@mouseup="end"
@mouseleave="end"
></div>
</div>
</template>
<style lang="scss" scoped>
@import '@/scss/components/progressbar.scss';
</style>
+66
View File
@@ -0,0 +1,66 @@
<script setup lang="ts">
import SortableList from './SortableList.vue';
import {
beautifyTime
} from '@/ts/util/time';
import player from '@/ts/player';
const queue = player.queue;
const idx = player.queueIdx;
const isPlaying = player.isPlaying;
</script>
<template>
<div>
<div>
<button>
<i class="fa-solid fa-plus"></i>
Add
</button>
<button>
<i class="fa-solid fa-xmark"></i>
Clear
</button>
<button>
<i class="fa-solid fa-save"></i>
Save
</button>
<!-- TODO: Should only appear when sharing -->
<button>
<i class="fa-solid fa-paper-plane"></i>
Transmit
</button>
</div>
<div>
<SortableList v-slot="{ item: song, index }" v-model="queue">
<div class="song-cover-wrapper">
<img
v-if="song.cover"
:src="song.cover"
alt="Song cover"
class="song-cover"
>
<i v-else class="fa-solid fa-music song-cover"></i>
<div v-if="isPlaying && index === idx" class="playing-symbols">
<div id="bar-1" class="playing-bar"></div>
<div id="bar-2" class="playing-bar"></div>
<div id="bar-3" class="playing-bar"></div>
</div>
<i v-else class="fa-solid fa-play play-pause" @click="() => player.playIndex( index )"></i>
<i class="fa-solid fa-pause play-pause" @click="player.pause"></i>
</div>
<div>
<h3>{{ song.name }}</h3>
<p>{{ song.artist }}</p>
<p>{{ beautifyTime( song.duration ) }}</p>
<p>{{ song['additional-info'] }}</p>
</div>
<div>
<i class="fa-solid fa-trash-can"></i>
<i class="fa-solid fa-pen-to-square"></i>
</div>
</SortableList>
</div>
</div>
</template>
+160
View File
@@ -0,0 +1,160 @@
<script setup lang="ts" generic="T">
import {
computed,
ref,
useTemplateRef
} from 'vue';
const array = defineModel<T[]>( {
'required': true
} );
defineSlots<{
default( props: {
'item': T,
'index': number
} ): void
}>();
const emit = defineEmits<{
( e: 'dropped' ): void
}>();
const scrollContainer = useTemplateRef( 'scroll-container' );
const movingIdx = ref( -1 );
const movingCurrentIdx = ref( -1 );
const movableSize = ref( -1 );
const offset = ref( -1 );
let mover = -1;
let moveSpeed = 0;
const start = ( ev: MouseEvent, idx: number ) => {
movableSize.value = document.getElementById( 'movable-' + idx )!.scrollHeight;
offset.value = ev.y - ( movableSize.value / 2 );
movingIdx.value = idx;
movingCurrentIdx.value = idx;
};
const move = ( ev: MouseEvent ) => {
if ( movingIdx.value < 0 ) return;
offset.value = ev.y - ( movableSize.value / 2 );
const relToContainer = ev.y - scrollContainer.value!.offsetTop;
const height = scrollContainer.value!.offsetHeight;
// Scrolling up and down
if ( relToContainer < 50 ) {
if ( relToContainer < 0 )
moveSpeed = -20;
else
moveSpeed = -Math.ceil( Math.max( 1, Math.min( 5 / relToContainer, 10 ) ) );
startTracker();
} else if ( relToContainer > height - 50 ) {
if ( height - relToContainer < 0 )
moveSpeed = 20;
else
moveSpeed = Math.ceil( Math.max( 1, Math.min( 5 / ( height - relToContainer ), 10 ) ) );
startTracker();
} else {
if ( mover >= 0 ) {
try {
clearInterval( mover );
mover = -1;
} catch { /* empty */ }
}
}
// Determine current index to insert
const percentage = ( relToContainer + scrollContainer.value!.scrollTop ) / scrollContainer.value!.scrollHeight;
const newIdx = Math.min( Math.max( Math.round( percentage * array.value.length ), 0 ), array.value.length - 1 );
if ( newIdx !== movingIdx.value )
movingCurrentIdx.value = newIdx;
else
movingCurrentIdx.value = movingIdx.value;
};
const startTracker = () => {
if ( mover < 0 ) {
mover = setInterval( () => {
scrollContainer.value!.scrollTop += moveSpeed;
}, 100 );
}
};
const end = () => {
offset.value = -1;
try {
clearInterval( mover );
mover = -1;
} catch { /* empty */ }
const before = array.value.slice( 0, movingIdx.value );
const after = array.value.slice( movingIdx.value + 1 );
const el = array.value[ movingIdx.value ]!;
array.value = before.concat( after );
array.value.splice( movingCurrentIdx.value, 0, el );
movingIdx.value = -1;
movingCurrentIdx.value = -1;
emit( 'dropped' );
};
const style = computed( () => {
return ( index: number ) => {
if ( movingIdx.value === array.value.length - 1 && movingIdx.value === index + 1
&& movingCurrentIdx.value === movingIdx.value )
return `margin-bottom: ${ movableSize.value }px;`;
if ( movingIdx.value >= 0 ) {
if ( movingIdx.value === index ) {
// Current item is moved
return `top: ${ offset.value }px;`;
} else {
// Current item is not moved
if ( movingCurrentIdx.value === array.value.length - 1 && index === movingCurrentIdx.value ) {
// Create gap below
return `margin-bottom: ${ movableSize.value }px;`;
} else if ( movingCurrentIdx.value === index
|| ( movingCurrentIdx.value === index - 1 && movingIdx.value == index - 1 ) ) {
// Create a gap above
return `margin-top: ${ movableSize.value }px;`;
}
}
}
return '';
};
} );
</script>
<template>
<div ref="scroll-container" class="sortable-list">
<div
v-if="movingIdx >= 0"
class="grip-target"
@mousemove="move"
@mouseleave="end"
@mouseup="end"
>
</div>
<div
v-for="(item, index) in array"
:id="'movable-' + index"
:key="index"
:class="[ 'movable', movingIdx === index ? 'moving' : undefined ]"
:style="style(index)"
>
<slot :item="item" :index="index"></slot>
<i class="fa-solid fa-grip-vertical" @mousedown="( e ) => start( e, index )"></i>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '@/scss/components/sortablelist.scss'
</style>
+1
View File
@@ -1,3 +1,4 @@
import '@fortawesome/fontawesome-free/css/all.css';
import App from './App.vue'; import App from './App.vue';
import { import {
createApp createApp
+110
View File
@@ -0,0 +1,110 @@
.mp-player {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.fa-solid {
font-size: 2.5rem;
cursor: pointer;
transition: font-size ease 0.5s;
user-select: none;
&:hover {
font-size: 2.7rem;
}
}
>.controls {
display: flex;
align-items: center;
justify-content: center;
>.play-pause {
cursor: pointer;
&.paused {
>.fa-pause {
display: none;
}
>.fa-play {
display: unset;
}
}
}
>.quick-seek {
position: relative;
&::after {
content: "10";
font-family: sans-serif;
font-weight: bold;
font-size: 40%;
position: absolute;
left: 0;
top: 5%;
height: 95%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
}
>.time {
display: flex;
width: 85%;
margin-left: auto;
margin-right: auto;
>.duration {
margin-left: auto;
}
}
.progress {
width: 95%;
}
>.bottom-bar {
>.fa-shuffle {
&.active {
background-color: var(--primary-color);
color: var(--secondary-color);
border-radius: 10px;
}
}
>.repeat {
position: relative;
padding: 5px;
&.once,
&.all {
background-color: var(--primary-color);
color: var(--secondary-color);
border-radius: 10px;
}
&.once::after {
position: absolute;
content: "1";
font-size: 40%;
font-family: sans-serif;
top: 5px;
right: 5px;
background-color: var(--accent-color);
width: 1.5em;
height: 1.5em;
display: flex;
align-items: center;
justify-content: center;
border-radius: 1em;
line-height: 100%;
}
}
}
}
+37
View File
@@ -0,0 +1,37 @@
.progressbar {
position: relative;
width: 100%;
cursor: pointer;
user-select: none;
>.back {
width: 100%;
background-color: var(--accent-background);
height: 10px;
border-radius: 10px;
overflow: hidden;
>div {
background-color: var(--accent-color);
height: 10px;
}
}
>.click-target {
width: 100%;
height: 10px;
position: absolute;
top: 0;
left: 0;
z-index: 10;
&.active {
cursor: grabbing;
width: 100vw;
height: 100vh;
transition: none;
position: fixed;
z-index: 10000;
}
}
}
+25
View File
@@ -0,0 +1,25 @@
.sortable-list {
overflow-y: scroll;
overflow-x: hidden;
height: 100%;
position: relative;
>.grip-target {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
cursor: grabbing;
}
>.movable {
&.moving {
position: fixed;
}
>i {
cursor: move;
}
}
}
+1 -1
View File
@@ -45,5 +45,5 @@ export interface Song {
/** /**
* Song duration * Song duration
*/ */
'duration': string; 'duration': number;
} }
+45 -5
View File
@@ -1,20 +1,26 @@
import {
addSongList,
shuffleList
} from './playlists/add';
import { import {
currentSource, currentSource,
isPlaying,
queue,
queueIdx, queueIdx,
rawQueue,
repeat, repeat,
shuffle, shuffle,
sources sources
} from './state'; } from './state';
import { import {
duration,
playbackPercentage,
startTracking, startTracking,
stopTracking stopTracking
} from './status-tracking'; } from './status-tracking';
import type { import type {
RepeatMode RepeatMode
} from '../dtype/player'; } from '../dtype/player';
import {
addSongList
} from './playlists/add';
import { import {
playIndex playIndex
} from './playlists'; } from './playlists';
@@ -31,6 +37,8 @@ const prev = () => {
const play = () => { const play = () => {
if ( currentSource.value === '' ) return; if ( currentSource.value === '' ) return;
isPlaying.value = true;
sources[currentSource.value]?.play(); sources[currentSource.value]?.play();
startTracking(); startTracking();
}; };
@@ -38,6 +46,8 @@ const play = () => {
const pause = () => { const pause = () => {
if ( currentSource.value === '' ) return; if ( currentSource.value === '' ) return;
isPlaying.value = false;
sources[currentSource.value]?.pause(); sources[currentSource.value]?.pause();
stopTracking(); stopTracking();
}; };
@@ -70,11 +80,34 @@ const back10 = () => {
seekTo( ( ( ( source?.getPlaybackPos() ?? 0 ) * duration ) - 10 ) / duration ); seekTo( ( ( ( source?.getPlaybackPos() ?? 0 ) * duration ) - 10 ) / duration );
}; };
/**
* Turn on or off shuffle
* @param enabled - Whether to enable or disable shuffle
*/
const setShuffle = ( enabled: boolean ) => { const setShuffle = ( enabled: boolean ) => {
// TODO: Shuffle the order
shuffle.value = enabled; shuffle.value = enabled;
if ( enabled ) {
shuffleList();
queueIdx.value = 0;
} else {
const curr = queue.value[queueIdx.value];
queue.value = [];
for ( let i = 0; i < rawQueue.value.length; i++ ) {
if ( rawQueue.value[i] === curr )
queueIdx.value = i;
queue.value.push( rawQueue.value[i]! );
}
}
}; };
/**
* Change the repeat mode
* @param mode - The repeat mode to switch into
*/
const setRepeat = ( mode: RepeatMode ) => { const setRepeat = ( mode: RepeatMode ) => {
repeat.value = mode; repeat.value = mode;
}; };
@@ -99,5 +132,12 @@ export default {
getSources, getSources,
addSongFromSource, addSongFromSource,
next, next,
prev prev,
queue,
queueIdx,
duration,
playbackPercentage,
repeat,
shuffle,
isPlaying
}; };
+100 -7
View File
@@ -1,14 +1,107 @@
import {
queue,
queueIdx,
rawQueue,
shuffle
} from '../state';
import type { import type {
Song Song
} from '@/ts/dtype/playlist'; } from '@/ts/dtype/playlist';
import {
queue
} from '../state';
export const addSongList = ( songs: Song[] ) => { export const addSongList = ( songs: Song[], first: boolean = false ) => {
for ( const song of songs ) { if ( first ) {
queue.value.push( song ); // FIXME: Needs to insert at current index in queue, but where for rawQueue?
// Probably at end if shuffled, else insert into queue, then copy
rawQueue.value = songs.concat( rawQueue.value );
queue.value = songs.concat( queue.value );
} else {
rawQueue.value = rawQueue.value.concat( songs );
queue.value = queue.value.concat( songs );
} }
}; };
export const shuffleList = () => {}; export const clearQueue = () => {
queue.value = [];
rawQueue.value = [];
};
/**
* Remove a song from the given queue index
* @param idx - The index to remove at
* @returns True if successful, False if not
*/
export const removeSong = ( idx: number ) => {
if ( idx <= queueIdx.value ) return false;
const removed = queue.value.splice( idx, 1 )[0];
// TODO: Check that this works
for ( let i = 0; i < rawQueue.value.length; i++ ) {
if ( rawQueue.value[ i ] === removed ) {
rawQueue.value.splice( i, 1 );
}
}
return true;
};
/**
* Move a song in the queue
* @param idx - The index to move
* @param newIdx - The index to move it to
* @returns True if successful, false if constraints were violated
*/
export const moveSong = ( idx: number, newIdx: number ) => {
if ( idx === newIdx ) return true;
if ( idx <= queueIdx.value || newIdx <= queueIdx.value || idx > queue.value.length || newIdx > queue.value.length )
return false;
// Affects only the queue if shuffled, else copy the queue into rawQueue
const movedEl = queue.value[ idx ]!;
if ( idx < newIdx )
for ( let i = idx + 1; i < Math.min( queue.value.length, newIdx + 1 ); i++ ) {
queue.value[ i - 1 ] = queue.value[ i ]!;
}
else if ( idx > newIdx )
for ( let i = idx + 1; i > Math.max( 0, newIdx + 1 ); i-- ) {
queue.value[ i ] = queue.value[ i - 1 ]!;
}
queue.value[ newIdx ] = movedEl;
if ( !shuffle.value ) {
// If we don't shuffle, the rawQueue should also be updated
rawQueue.value = [];
for ( const song of queue.value ) {
rawQueue.value.push( song );
}
}
return true;
};
/** Shuffle the song list */
export const shuffleList = () => {
const shuffled = rawQueue.value
.map( ( val, idx ) => {
return {
val,
idx,
'sort': Math.random()
};
} ).sort( ( a, b ) => a.sort - b.sort );
const q: Song[] = [];
// Make current song the first in the list
q.push( rawQueue.value[queueIdx.value]! );
for ( const el of shuffled ) {
if ( el.idx !== queueIdx.value )
q.push( el.val );
}
queue.value = q;
};
+2 -4
View File
@@ -18,6 +18,8 @@ export const sources: {
export const currentSource = ref( '' ); export const currentSource = ref( '' );
export const rawQueue: Ref<Playlist> = ref( [] );
export const queueIdx = ref( 0 ); export const queueIdx = ref( 0 );
export const queue: Ref<Playlist> = ref( [] ); export const queue: Ref<Playlist> = ref( [] );
@@ -27,7 +29,3 @@ export const isPlaying = ref( false );
export const shuffle = ref( false ); export const shuffle = ref( false );
export const repeat: Ref<RepeatMode> = ref( 'off' ); export const repeat: Ref<RepeatMode> = ref( 'off' );
export const playbackPercentage = ref( 0 );
export const duration = ref( 0 );
+48 -3
View File
@@ -1,4 +1,49 @@
// Tracking of time and player status import {
export const startTracking = () => {}; currentSource,
queueIdx,
repeat,
sources
} from './state';
import {
playIndex
} from './playlists';
import {
ref
} from 'vue';
export const stopTracking = () => {}; // Tracking of time and player status
let interval = -1;
export const playbackPercentage = ref( 0 );
export const duration = ref( 0 );
export const startTracking = () => {
if ( interval === -1 ) {
interval = setInterval( tracker, 250 );
}
duration.value = sources[currentSource.value]?.getDuration() ?? -1;
};
const tracker = () => {
playbackPercentage.value = sources[currentSource.value]?.getPlaybackPos() ?? -1;
if ( playbackPercentage.value > duration.value ) {
if ( repeat.value === 'one' ) {
sources[currentSource.value]?.seekTo( 0 );
} else {
stopTracking();
playIndex( queueIdx.value + 1 );
}
}
};
export const stopTracking = () => {
try {
clearInterval( interval );
interval = -1;
} catch {
// empty
}
};
+23
View File
@@ -0,0 +1,23 @@
/**
* Get a human-readable representation of time
* @param time - The time to convert, in seconds
* @returns The time, in human-readable format
*/
export const beautifyTime = ( time: number ): string => {
const helper = ( t: number, depth: number ): string => {
if ( depth > 2 )
return `${ Math.floor( t / 60 ) }:${ t % 60 < 10 ? '0' : '' }${ Math.floor( t % 60 ) }`;
else if ( Math.floor( t / 60 ) > 0 )
return `${ helper( Math.floor( t / 60 ), depth + 1 ) }:${ t % 60 < 10 ? '0' : '' }${ Math.floor( t % 60 ) }`;
else
return `${ t % 60 < 10 ? '0' : '' }${ Math.floor( t % 60 ) }`;
};
if ( time < 0 ) {
return '-:--';
} else if ( time == 0 ) {
return '0:00';
}
return helper( Math.round( time ), 1 );
};
+7
View File
@@ -1,5 +1,12 @@
<script setup lang="ts">
import QueueViewer from '@/components/QueueViewer.vue';
</script>
<template> <template>
<div> <div>
<h1>Player</h1> <h1>Player</h1>
<QueueViewer />
</div> </div>
</template> </template>
+31
View File
@@ -1,5 +1,36 @@
<script setup lang="ts">
import SortableList from '@/components/SortableList.vue';
import {
ref
} from 'vue';
const items = ref( [
'test',
'test2',
'test3',
'test4',
'test5',
'test6',
'test7',
'test8',
'test9',
'test10'
] );
</script>
<template> <template>
<div> <div>
<h1>MusicPlayer</h1> <h1>MusicPlayer</h1>
<div class="test">
<SortableList v-slot="{ item, index }" v-model="items">
{{ item }} at {{ index }}
</SortableList>
</div>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.test {
height: 150px;
}
</style>