318 lines
11 KiB
Vue
318 lines
11 KiB
Vue
<template>
|
|
<div class="card">
|
|
<div class="card-header bg-primary-dark">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-fill text-nowrap">
|
|
<h5 class="card-title">{{ lang_header }}</h5>
|
|
</div>
|
|
<div class="flex-shrink-0 pl-3">
|
|
<volume-slider v-model.number="volume"></volume-slider>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="control-group d-flex justify-content-center">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-sm btn-success" v-if="!playing || paused" v-on:click="play">
|
|
<icon icon="play_arrow"></icon>
|
|
</button>
|
|
<button class="btn btn-sm btn-warning" v-if="playing && !paused" v-on:click="togglePause()">
|
|
<icon icon="pause"></icon>
|
|
</button>
|
|
<button class="btn btn-sm" v-on:click="previous()">
|
|
<icon icon="fast_rewind"></icon>
|
|
</button>
|
|
<button class="btn btn-sm" v-on:click="next()">
|
|
<icon icon="fast_forward"></icon>
|
|
</button>
|
|
<button class="btn btn-sm btn-danger" v-on:click="stop()">
|
|
<icon icon="stop"></icon>
|
|
</button>
|
|
<button class="btn btn-sm" v-on:click="cue()" v-bind:class="{ 'btn-primary': passThrough }">
|
|
{{ $gettext('Cue') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3" v-if="playing">
|
|
|
|
<div class="d-flex flex-row mb-2">
|
|
<div class="flex-shrink-0 pt-1 pr-2">{{ prettifyTime(position) }}</div>
|
|
<div class="flex-fill">
|
|
<input type="range" min="0" max="100" step="0.1" class="custom-range slider"
|
|
v-bind:value="seekingPosition"
|
|
v-on:mousedown="isSeeking = true"
|
|
v-on:mousemove="doSeek($event)"
|
|
v-on:mouseup="isSeeking = false">
|
|
</div>
|
|
<div class="flex-shrink-0 pt-1 pl-2">{{ prettifyTime(duration) }}</div>
|
|
</div>
|
|
|
|
<div class="progress mb-1">
|
|
<div class="progress-bar" v-bind:style="{ width: volumeLeft+'%' }"></div>
|
|
</div>
|
|
<div class="progress">
|
|
<div class="progress-bar" v-bind:style="{ width: volumeRight+'%' }"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group mt-2">
|
|
<div class="custom-file">
|
|
<input v-bind:id="id + '_files'" type="file" class="custom-file-input files" accept="audio/*"
|
|
multiple="multiple" v-on:change="addNewFiles($event.target.files)">
|
|
<label v-bind:for="id + '_files'" class="custom-file-label">
|
|
{{ $gettext('Add Files to Playlist') }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group mb-0">
|
|
<div class="controls">
|
|
<div class="custom-control custom-checkbox custom-control-inline">
|
|
<input v-bind:id="id + '_playthrough'" type="checkbox" class="custom-control-input"
|
|
v-model="playThrough">
|
|
<label v-bind:for="id + '_playthrough'" class="custom-control-label">
|
|
{{ $gettext('Continuous Play') }}
|
|
</label>
|
|
</div>
|
|
<div class="custom-control custom-checkbox custom-control-inline">
|
|
<input v-bind:id="id + '_loop'" type="checkbox" class="custom-control-input" v-model="loop">
|
|
<label v-bind:for="id + '_loop'" class="custom-control-label">
|
|
{{ $gettext('Repeat') }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="list-group list-group-flush" v-if="files.length > 0">
|
|
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start"
|
|
v-for="(rowFile, rowIndex) in files" v-bind:class="{ active: rowIndex === fileIndex }"
|
|
v-on:click.prevent="play({ fileIndex: rowIndex })">
|
|
<div class="d-flex w-100 justify-content-between">
|
|
<h5 class="mb-0">{{
|
|
rowFile.metadata.title ? rowFile.metadata.title : lang_unknown_title
|
|
}}</h5>
|
|
<small class="pt-1">{{ prettifyTime(rowFile.audio.length) }}</small>
|
|
</div>
|
|
<p class="mb-0">{{ rowFile.metadata.artist ? rowFile.metadata.artist : lang_unknown_artist }}</p>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import track from './Track.js';
|
|
import _ from 'lodash';
|
|
import Icon from '~/components/Common/Icon';
|
|
import VolumeSlider from "~/components/Public/WebDJ/VolumeSlider";
|
|
|
|
export default {
|
|
components: {VolumeSlider, Icon},
|
|
extends: track,
|
|
data() {
|
|
return {
|
|
'fileIndex': -1,
|
|
'files': [],
|
|
|
|
'volume': 100,
|
|
'duration': 0.0,
|
|
'playThrough': true,
|
|
'loop': false,
|
|
|
|
'isSeeking': false,
|
|
'seekPosition': 0,
|
|
'mixGainObj': null
|
|
};
|
|
},
|
|
computed: {
|
|
lang_header () {
|
|
return (this.id === 'playlist_1')
|
|
? this.$gettext('Playlist 1')
|
|
: this.$gettext('Playlist 2');
|
|
},
|
|
lang_unknown_title () {
|
|
return this.$gettext('Unknown Title');
|
|
},
|
|
lang_unknown_artist () {
|
|
return this.$gettext('Unknown Artist');
|
|
},
|
|
positionPercent () {
|
|
return (100.0 * this.position / parseFloat(this.duration));
|
|
},
|
|
seekingPosition () {
|
|
return (this.isSeeking) ? this.seekPosition : this.positionPercent;
|
|
}
|
|
},
|
|
props: {
|
|
id: String
|
|
},
|
|
mounted () {
|
|
this.mixGainObj = this.getStream().context.createGain();
|
|
this.mixGainObj.connect(this.getStream().webcast);
|
|
this.sink = this.mixGainObj;
|
|
|
|
this.$root.$on('new-mixer-value', this.setMixGain);
|
|
this.$root.$on('new-cue', this.onNewCue);
|
|
},
|
|
methods: {
|
|
prettifyTime(time) {
|
|
if (typeof time === 'undefined') {
|
|
return 'N/A';
|
|
}
|
|
|
|
let hours = parseInt(time / 3600);
|
|
time %= 3600;
|
|
let minutes = parseInt(time / 60);
|
|
let seconds = parseInt(time % 60);
|
|
|
|
if (minutes < 10) {
|
|
minutes = '0' + minutes;
|
|
}
|
|
if (seconds < 10) {
|
|
seconds = '0' + seconds;
|
|
}
|
|
|
|
if (hours > 0) {
|
|
return hours + ':' + minutes + ':' + seconds;
|
|
} else {
|
|
return minutes + ':' + seconds;
|
|
}
|
|
},
|
|
cue() {
|
|
this.resumeStream();
|
|
this.$root.$emit('new-cue', (this.passThrough) ? 'off' : this.id);
|
|
},
|
|
|
|
onNewCue(new_cue) {
|
|
this.passThrough = (new_cue === this.id);
|
|
},
|
|
|
|
setMixGain(new_value) {
|
|
if (this.id === 'playlist_1') {
|
|
this.mixGainObj.gain.value = 1.0 - new_value;
|
|
} else {
|
|
this.mixGainObj.gain.value = new_value;
|
|
}
|
|
},
|
|
|
|
addNewFiles (newFiles) {
|
|
_.each(newFiles, (file) => {
|
|
file.readTaglibMetadata((data) => {
|
|
this.files.push({
|
|
file: file,
|
|
audio: data.audio,
|
|
metadata: data.metadata || { title: '', artist: '' }
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
play (options) {
|
|
this.resumeStream();
|
|
|
|
if (this.paused) {
|
|
this.togglePause();
|
|
return;
|
|
}
|
|
|
|
this.stop();
|
|
|
|
if (!(this.file = this.selectFile(options))) {
|
|
return;
|
|
}
|
|
|
|
this.prepare();
|
|
|
|
return this.getStream().createFileSource(this.file, this, (source) => {
|
|
let ref1;
|
|
this.source = source;
|
|
this.source.connect(this.destination);
|
|
if (this.source.duration != null) {
|
|
this.duration = this.source.duration();
|
|
} else {
|
|
if (((ref1 = this.file.audio) != null ? ref1.length : void 0) != null) {
|
|
this.duration = parseFloat(this.file.audio.length);
|
|
}
|
|
}
|
|
|
|
this.source.play(this.file);
|
|
|
|
this.$root.$emit('metadata-update', {
|
|
title: this.file.metadata.title,
|
|
artist: this.file.metadata.artist
|
|
});
|
|
|
|
this.playing = true;
|
|
this.paused = false;
|
|
});
|
|
},
|
|
|
|
selectFile (options = {}) {
|
|
if (this.files.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (options.fileIndex) {
|
|
this.fileIndex = options.fileIndex;
|
|
} else {
|
|
this.fileIndex += options.backward ? -1 : 1;
|
|
if (this.fileIndex < 0) {
|
|
this.fileIndex = this.files.length - 1;
|
|
}
|
|
|
|
if (this.fileIndex >= this.files.length) {
|
|
if (options.isAutoPlay && !this.loop) {
|
|
this.fileIndex = -1;
|
|
return;
|
|
}
|
|
|
|
if (this.fileIndex < 0) {
|
|
this.fileIndex = this.files.length - 1;
|
|
} else {
|
|
this.fileIndex = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.files[this.fileIndex];
|
|
},
|
|
|
|
previous () {
|
|
if (!this.playing) {
|
|
return;
|
|
}
|
|
|
|
return this.play({
|
|
backward: true
|
|
});
|
|
},
|
|
|
|
next () {
|
|
if (!this.playing) {
|
|
return;
|
|
}
|
|
|
|
return this.play();
|
|
},
|
|
|
|
onEnd () {
|
|
this.stop();
|
|
|
|
if (this.playThrough) {
|
|
return this.play({
|
|
isAutoPlay: true
|
|
});
|
|
}
|
|
},
|
|
|
|
doSeek (e) {
|
|
if (this.isSeeking) {
|
|
this.seekPosition = e.target.value;
|
|
this.seek(this.seekPosition / 100);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
</script>
|