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"
|
||||
},
|
||||
"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",
|
||||
"vue": "^3.5.40",
|
||||
"vue-router": "^5.2.0"
|
||||
@@ -24,14 +27,14 @@
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"eslint-plugin-vue": "^10.10.0",
|
||||
"globals": "^17.11.0",
|
||||
"musickitjs-v3-types": "^1.0.0",
|
||||
"npm-run-all2": "^9.0.2",
|
||||
"sass-embedded": "^1.103.1",
|
||||
"typescript": "~6.0.0",
|
||||
"typescript-eslint": "^8.67.0",
|
||||
"vite": "^8.1.5",
|
||||
"vite-plugin-vue-devtools": "^8.1.5",
|
||||
"vue-tsc": "^3.3.7",
|
||||
"musickitjs-v3-types": "^1.0.0"
|
||||
"vue-tsc": "^3.3.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.12.0"
|
||||
|
||||
+3
-3
@@ -57,7 +57,7 @@
|
||||
--secondary-color: white;
|
||||
--background-color: rgb(221, 221, 221);
|
||||
--nav-background: white;
|
||||
--hover-color: #00457a;
|
||||
--accent-color: #00457a;
|
||||
--popup-color: rgb(224, 224, 224);
|
||||
--overlay-color: rgba(0, 0, 0, 0.7);
|
||||
--PI: 3.14159265358979;
|
||||
@@ -74,7 +74,7 @@
|
||||
--background-color: rgb(32, 32, 32);
|
||||
--nav-background: rgb(54, 54, 54);
|
||||
--popup-color: rgb(58, 58, 58);
|
||||
--hover-color: #007ddd;
|
||||
--accent-color: #007ddd;
|
||||
--overlay-color: rgba(104, 104, 104, 0.575);
|
||||
--gray-color: rgb(207, 207, 207);
|
||||
--footer-background: rgb(53, 53, 53);
|
||||
@@ -90,7 +90,7 @@
|
||||
--background-color: rgb(32, 32, 32);
|
||||
--nav-background: rgb(54, 54, 54);
|
||||
--popup-color: rgb(58, 58, 58);
|
||||
--hover-color: #007ddd;
|
||||
--accent-color: #007ddd;
|
||||
--overlay-color: rgba(104, 104, 104, 0.575);
|
||||
--gray-color: rgb(207, 207, 207);
|
||||
--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>
|
||||
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.popup-backdrop {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@@ -37,13 +37,12 @@
|
||||
transition: all 0.5s;
|
||||
transform: scale(1);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
&.hidden {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.popup-main {
|
||||
>.popup-main {
|
||||
width: 40%;
|
||||
height: 50%;
|
||||
background-color: var( --secondary-color );
|
||||
@@ -52,9 +51,8 @@
|
||||
position: relative;
|
||||
overflow-y: scroll;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
>.close-icon {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
@@ -62,4 +60,6 @@
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</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 {
|
||||
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
|
||||
*/
|
||||
'duration': string;
|
||||
'duration': number;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import {
|
||||
addSongList,
|
||||
shuffleList
|
||||
} from './playlists/add';
|
||||
import {
|
||||
currentSource,
|
||||
isPlaying,
|
||||
queue,
|
||||
queueIdx,
|
||||
rawQueue,
|
||||
repeat,
|
||||
shuffle,
|
||||
sources
|
||||
} from './state';
|
||||
import {
|
||||
duration,
|
||||
playbackPercentage,
|
||||
startTracking,
|
||||
stopTracking
|
||||
} from './status-tracking';
|
||||
import type {
|
||||
RepeatMode
|
||||
} from '../dtype/player';
|
||||
import {
|
||||
addSongList
|
||||
} from './playlists/add';
|
||||
import {
|
||||
playIndex
|
||||
} from './playlists';
|
||||
@@ -31,6 +37,8 @@ const prev = () => {
|
||||
const play = () => {
|
||||
if ( currentSource.value === '' ) return;
|
||||
|
||||
isPlaying.value = true;
|
||||
|
||||
sources[currentSource.value]?.play();
|
||||
startTracking();
|
||||
};
|
||||
@@ -38,6 +46,8 @@ const play = () => {
|
||||
const pause = () => {
|
||||
if ( currentSource.value === '' ) return;
|
||||
|
||||
isPlaying.value = false;
|
||||
|
||||
sources[currentSource.value]?.pause();
|
||||
stopTracking();
|
||||
};
|
||||
@@ -70,11 +80,34 @@ const back10 = () => {
|
||||
seekTo( ( ( ( source?.getPlaybackPos() ?? 0 ) * duration ) - 10 ) / duration );
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn on or off shuffle
|
||||
* @param enabled - Whether to enable or disable shuffle
|
||||
*/
|
||||
const setShuffle = ( enabled: boolean ) => {
|
||||
// TODO: Shuffle the order
|
||||
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 ) => {
|
||||
repeat.value = mode;
|
||||
};
|
||||
@@ -99,5 +132,12 @@ export default {
|
||||
getSources,
|
||||
addSongFromSource,
|
||||
next,
|
||||
prev
|
||||
prev,
|
||||
queue,
|
||||
queueIdx,
|
||||
duration,
|
||||
playbackPercentage,
|
||||
repeat,
|
||||
shuffle,
|
||||
isPlaying
|
||||
};
|
||||
|
||||
@@ -1,14 +1,107 @@
|
||||
import {
|
||||
queue,
|
||||
queueIdx,
|
||||
rawQueue,
|
||||
shuffle
|
||||
} from '../state';
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
import {
|
||||
queue
|
||||
} from '../state';
|
||||
|
||||
export const addSongList = ( songs: Song[] ) => {
|
||||
for ( const song of songs ) {
|
||||
queue.value.push( song );
|
||||
export const addSongList = ( songs: Song[], first: boolean = false ) => {
|
||||
if ( first ) {
|
||||
// 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 rawQueue: Ref<Playlist> = ref( [] );
|
||||
|
||||
export const queueIdx = ref( 0 );
|
||||
|
||||
export const queue: Ref<Playlist> = ref( [] );
|
||||
@@ -27,7 +29,3 @@ export const isPlaying = ref( false );
|
||||
export const shuffle = ref( false );
|
||||
|
||||
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
|
||||
export const startTracking = () => {};
|
||||
import {
|
||||
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>
|
||||
<div>
|
||||
<h1>Player</h1>
|
||||
<QueueViewer />
|
||||
</div>
|
||||
</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>
|
||||
<div>
|
||||
<h1>MusicPlayer</h1>
|
||||
<div class="test">
|
||||
<SortableList v-slot="{ item, index }" v-model="items">
|
||||
{{ item }} at {{ index }}
|
||||
</SortableList>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.test {
|
||||
height: 150px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user