mirror of
https://github.com/janishutz/MusicPlayer.git
synced 2026-09-10 14:25:24 +02:00
Compare commits
7
Commits
5b223875bd
...
5f6e034dac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f6e034dac | ||
|
|
eead8b558a | ||
|
|
4753d998b3 | ||
|
|
9bf88e033e | ||
|
|
0e155831d7 | ||
|
|
1d492d5922 | ||
|
|
4d9064f5c2 |
+5
-2
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Hello World
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -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,13 +37,12 @@
|
|||||||
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 );
|
||||||
@@ -52,9 +51,8 @@
|
|||||||
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;
|
||||||
@@ -62,4 +60,6 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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,3 +1,4 @@
|
|||||||
|
import '@fortawesome/fontawesome-free/css/all.css';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import {
|
import {
|
||||||
createApp
|
createApp
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -45,5 +45,5 @@ export interface Song {
|
|||||||
/**
|
/**
|
||||||
* Song duration
|
* Song duration
|
||||||
*/
|
*/
|
||||||
'duration': string;
|
'duration': number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 );
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 );
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user