First release

This commit is contained in:
exttex
2020-08-28 23:06:19 +02:00
commit b94234c8e7
50 changed files with 18231 additions and 0 deletions

View File

@ -0,0 +1,152 @@
<template>
<div>
<v-list-item two-line @click='click' v-if='!card'>
<v-hover v-slot:default='{hover}'>
<v-list-item-avatar>
<v-img :src='album.art.thumb'></v-img>
<v-overlay absolute :value='hover'>
<v-btn icon large @click.stop='play'>
<v-icon>mdi-play</v-icon>
</v-btn>
</v-overlay>
</v-list-item-avatar>
</v-hover>
<v-list-item-content>
<v-list-item-title>{{album.title}}</v-list-item-title>
<v-list-item-subtitle>{{album.artistString}}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<!-- Context menu -->
<v-menu v-model='menu' offset-y offset-x absolue>
<template v-slot:activator="{on, attrs}">
<v-btn v-on='on' v-bind='attrs' icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<!-- Play album -->
<v-list-item dense @click='play'>
<v-list-item-icon>
<v-icon>mdi-play</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Play</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Add to library -->
<v-list-item dense @click='addLibrary'>
<v-list-item-icon>
<v-icon>mdi-heart</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Add to library</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Download -->
<v-list-item dense @click='download'>
<v-list-item-icon>
<v-icon>mdi-download</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Download</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</v-list-item-action>
</v-list-item>
<v-card v-if='card' max-width='175px' max-height='210px' @click='click'>
<v-hover v-slot:default='{hover}'>
<div>
<v-img :src='album.art.thumb'>
</v-img>
<v-overlay absolute :value='hover' opacity='0.5'>
<v-btn fab small color='white' @click.stop='play'>
<v-icon color='black'>mdi-play</v-icon>
</v-btn>
</v-overlay>
</div>
</v-hover>
<div class='pa-2 text-subtitle-2 text-center text-truncate'>{{album.title}}</div>
</v-card>
<DownloadDialog :tracks='album.tracks' v-if='downloadDialog' @close='downloadDialog = false'></DownloadDialog>
</div>
</template>
<script>
import DownloadDialog from '@/components/DownloadDialog.vue';
export default {
name: 'AlbumTile',
components: {DownloadDialog},
data() {
return {
menu: false,
hover: false,
downloadDialog: false
}
},
props: {
album: Object,
card: {
type: Boolean,
default: false
}
},
methods: {
async play() {
let album = this.album;
//Load album from API if tracks are missing
if (album.tracks.length == 0) {
let data = await this.$axios.get(`/album/${album.id}`)
album = data.data;
}
//Error handling
if (!album) return;
this.$root.queueSource = {
text: album.title,
source: 'album',
data: album.id
};
this.$root.replaceQueue(album.tracks);
this.$root.playIndex(0);
},
//On click navigate to details
click() {
this.$router.push({
path: '/album',
query: {album: JSON.stringify(this.album)}
});
this.$emit('clicked')
},
addLibrary() {
this.$axios.put(`/library/album?id=${this.album.id}`);
},
//Add to downloads
async download() {
//Fetch tracks if missing
let tracks = this.album.tracks;
if (!tracks || tracks.length == 0) {
let data = await this.$axios.get(`/album/${this.album.id}`)
tracks = data.data.tracks;
}
this.album.tracks = tracks;
this.downloadDialog = true;
}
}
};
</script>

View File

@ -0,0 +1,84 @@
<template>
<div>
<v-list-item @click='click' v-if='!card' :class='{dense: tiny}'>
<v-list-item-avatar v-if='!tiny'>
<v-img :src='artist.picture.thumb'></v-img>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>{{artist.name}}</v-list-item-title>
<v-list-item-subtitle v-if='!tiny'>{{$abbreviation(artist.fans)}} fans</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<!-- Context menu -->
<v-menu v-model='menu' offset-y offset-x absolue>
<template v-slot:activator="{on, attrs}">
<v-btn v-on='on' v-bind='attrs' icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<!-- Add library -->
<v-list-item dense @click='addLibrary'>
<v-list-item-icon>
<v-icon>mdi-heart</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Add to library</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</v-list-item-action>
</v-list-item>
<!-- Card version -->
<v-card max-height='200px' max-width='200px' v-if='card' @click='click'>
<div class='d-flex justify-center'>
<v-avatar size='150' class='ma-1'>
<v-img :src='artist.picture.thumb'>
</v-img>
</v-avatar>
</div>
<div class='pa-2 text-subtitle-2 text-center text-truncate'>{{artist.name}}</div>
</v-card>
</div>
</template>
<script>
export default {
name: 'ArtistTile',
data() {
return {
menu: false
}
},
props: {
artist: Object,
card: {
type: Boolean,
default: false,
},
tiny: {
type: Boolean,
default: false
}
},
methods: {
addLibrary() {
this.$axios.put(`/library/artist&id=${this.artist.id}`);
},
click() {
//Navigate to details
this.$router.push({
path: '/artist',
query: {artist: JSON.stringify(this.artist)}
});
this.$emit('clicked');
}
}
}
</script>

View File

@ -0,0 +1,35 @@
<template>
<div>
<v-card
width='225px'
height='100px'
:img='channel.image.thumb'
@click='click'
>
<v-container fill-height class='justify-center'>
<v-card-title class='font-weight-black text-truncate text-h6 pa-1'>{{channel.title}}</v-card-title>
</v-container>
</v-card>
</div>
</template>
<script>
export default {
name: 'DeezerChannel',
props: {
channel: Object
},
methods: {
click() {
console.log(this.channel.target);
this.$router.push({
path: '/page',
query: {target: this.channel.target}
});
}
}
}
</script>

View File

@ -0,0 +1,103 @@
<template>
<div>
<v-dialog v-model='show' max-width='420'>
<v-card>
<v-card-title class='headline'>
Download {{tracks.length}} tracks
</v-card-title>
<v-card-text class='pb-0'>
<v-select
label='Quality'
persistent-hint
:items='qualities'
v-model='qualityString'
:hint='"Estimated size: " + $filesize(estimatedSize)'
></v-select>
<v-checkbox
v-model='autostart'
label='Start downloading'
></v-checkbox>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click='$emit("close")'>Cancel</v-btn>
<v-btn text @click='download'>Download</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
name: 'DownloadDialog',
props: {
tracks: Array,
show: {
type: Boolean,
default: true
},
},
data() {
return {
shown: true,
qualities: [
'Settings quality',
'MP3 128kbps',
'MP3 320kbps',
'FLAC ~1441kbps'
],
qualityString: 'Settings quality',
autostart: true,
}
},
methods: {
//Get quality int from select
qualityInt() {
let i = this.qualities.indexOf(this.qualityString);
if (i == 1) return 1;
if (i == 2) return 3;
if (i == 3) return 9;
return this.$root.settings.downloadsQuality;
},
//Add files to download queue
async download() {
if (this.qualities.indexOf(this.qualityString) == 0 || !this.qualityString) {
await this.$axios.post(`/downloads`, this.tracks);
} else {
await this.$axios.post(`/downloads?q=${this.qualityInt()}`, this.tracks);
}
if (this.autostart) this.$axios.put('/download');
this.$emit("close");
}
},
computed: {
estimatedSize() {
let qi = this.qualityInt();
let duration = this.tracks.reduce((a, b) => a + (b.duration / 1000), 0);
//Magic numbers = bitrate / 8 * 1024 = bytes per second
switch (qi) {
case 1:
return duration * 16384;
case 3:
return duration * 40960;
case 9:
//FLAC is 1144, because more realistic
return duration * 146432;
}
return duration * this.$root.settings.downloadsQuality;
}
}
}
</script>

View File

@ -0,0 +1,45 @@
<template>
<v-list>
<v-overlay v-if='loading'>
<v-progress-circular indeterminate></v-progress-circular>
</v-overlay>
<v-lazy max-height="100" v-for='album in albums' :key='album.id'>
<AlbumTile :album='album'></AlbumTile>
</v-lazy>
</v-list>
</template>
<script>
import AlbumTile from '@/components/AlbumTile.vue';
export default {
name: 'LibraryAlbums',
data() {
return {
albums: [],
loading: false
}
},
methods: {
//Load data
async load() {
this.loading = true;
let res = await this.$axios.get(`/library/albums`);
if (res.data && res.data.data) {
this.albums = res.data.data;
}
this.loading = false;
}
},
components: {
AlbumTile
},
mounted() {
//Initial load
this.load();
}
}
</script>

View File

@ -0,0 +1,45 @@
<template>
<v-list>
<v-overlay v-if='loading'>
<v-progress-circular indeterminate></v-progress-circular>
</v-overlay>
<v-lazy max-height="100" v-for='artist in artists' :key='artist.id'>
<ArtistTile :artist='artist'></ArtistTile>
</v-lazy>
</v-list>
</template>
<script>
import ArtistTile from '@/components/ArtistTile.vue';
export default {
name: 'LibraryArtists',
components: {
ArtistTile
},
data() {
return {
artists: [],
loading: false
}
},
methods: {
//Load data
async load() {
this.loading = true;
let res = await this.$axios.get(`/library/artists`);
if (res.data && res.data.data) {
this.artists = res.data.data;
}
this.loading = false;
}
},
mounted() {
//Initial load
this.load();
}
}
</script>

View File

@ -0,0 +1,69 @@
<template>
<v-list>
<v-overlay v-if='loading'>
<v-progress-circular indeterminate></v-progress-circular>
</v-overlay>
<!-- Create playlist -->
<v-btn class='ma-2 ml-3' color='primary' @click='popup = true'>
<v-icon left>mdi-playlist-plus</v-icon>
Create new playlist
</v-btn>
<v-dialog max-width="400px" v-model='popup'>
<PlaylistPopup @created='playlistCreated'></PlaylistPopup>
</v-dialog>
<v-lazy max-height="100" v-for='(playlist, index) in playlists' :key='playlist.id'>
<PlaylistTile :playlist='playlist' @remove='removed(index)'></PlaylistTile>
</v-lazy>
</v-list>
</template>
<script>
import PlaylistTile from '@/components/PlaylistTile.vue';
import PlaylistPopup from '@/components/PlaylistPopup.vue';
export default {
name: 'LibraryPlaylists',
components: {
PlaylistTile, PlaylistPopup
},
data() {
return {
playlists: [],
loading: false,
popup: false
}
},
methods: {
//Load data
async load() {
this.loading = true;
let res = await this.$axios.get(`/library/playlists`);
if (res.data && res.data.data) {
this.playlists = res.data.data;
}
this.loading = false;
},
//Playlist created, update list
playlistCreated() {
this.popup = false;
this.playlists = [];
this.load();
},
//On playlist remove
removed(i) {
this.playlists.splice(i, 1);
}
},
mounted() {
//Initial load
this.load();
}
}
</script>

View File

@ -0,0 +1,98 @@
<template>
<v-list :height='height' class='overflow-y-auto' v-scroll.self='scroll'>
<v-lazy
v-for='(track, index) in tracks'
:key='index + "t" + track.id'
max-height="100"
><TrackTile :track='track' @click='play(index)' @remove='removedTrack(index)'>
</TrackTile>
</v-lazy>
<div class='text-center' v-if='loading'>
<v-progress-circular indeterminate></v-progress-circular>
</div>
</v-list>
</template>
<script>
import TrackTile from '@/components/TrackTile.vue';
export default {
name: 'LibraryTracks',
components: {
TrackTile
},
data() {
return {
loading: false,
tracks: [],
count: 0
}
},
props: {
height: String
},
methods: {
scroll(event) {
let loadOffset = event.target.scrollHeight - event.target.offsetHeight - 100;
if (event.target.scrollTop > loadOffset) {
if (!this.loading) this.load();
}
},
//Load initial data
initialLoad() {
this.loading = true;
this.$axios.get(`/library/tracks`).then((res) => {
this.tracks = res.data.data;
this.count = res.data.count;
this.loading = false;
});
},
//Load more tracks
load() {
if (this.tracks.length >= this.count) return;
this.loading = true;
//Library Favorites = playlist
let id = this.$root.profile.favoritesPlaylist;
let offset = this.tracks.length;
this.$axios.get(`/playlist/${id}?start=${offset}`).then((res) => {
this.tracks.push(...res.data.tracks);
this.loading = false;
});
},
//Load all tracks
async loadAll() {
this.loading = true;
let id = this.$root.profile.favoritesPlaylist;
let res = await this.$axios.get(`/playlist/${id}?full=iguess`);
if (res.data && res.data.tracks) {
this.tracks.push(...res.data.tracks.slice(this.tracks.length));
}
this.loading = false;
},
//Play track
async play(index) {
if (this.tracks.length < this.count) {
await this.loadAll();
}
this.$root.queue.source = {
text: 'Loved tracks',
source: 'playlist',
data: this.$root.profile.favoritesPlaylist
};
this.$root.replaceQueue(this.tracks);
this.$root.playIndex(index);
},
removedTrack(index) {
this.tracks.splice(index, 1);
}
},
mounted() {
this.initialLoad();
}
}
</script>

View File

@ -0,0 +1,109 @@
<template>
<div :style='"max-height: " + height' class='overflow-y-auto' ref='content'>
<div class='text-center my-4'>
<v-progress-circular indeterminate v-if='loading'></v-progress-circular>
</div>
<div v-if='!loading && lyrics' class='text-center'>
<div v-for='(lyric, index) in lyrics.lyrics' :key='lyric.offset' class='my-8 mx-4'>
<span
class='my-8'
:class='{"text-h6 font-weight-regular": !playingNow(index), "text-h5 font-weight-bold": playingNow(index)}'
:ref='"l"+index'
>
{{lyric.text}}
</span>
</div>
</div>
<!-- Error -->
<div v-if='!loading && !lyrics' class='pa-4 text-center'>
<span class='red--text text-h5'>
Error loading lyrics or lyrics not found!
</span>
</div>
</div>
</template>
<script>
export default {
name: 'Lyrics',
props: {
songId: String,
height: String
},
data() {
return {
cSongId: this.songId,
loading: true,
lyrics: null,
currentLyricIndex: 0,
}
},
methods: {
//Load data from API
async load() {
this.loading = true;
this.lyrics = null;
try {
let res = await this.$axios.get(`/lyrics/${this.songId}`);
if (res.data) this.lyrics = res.data;
} catch (e) {true;}
this.loading = false;
},
//Wether current lyric is playing rn
playingNow(i) {
if (!this.$root.audio) return false;
//First & last lyric check
if (i == this.lyrics.lyrics.length - 1) {
if (this.lyrics.lyrics[i].offset <= this.$root.position) return true;
return false;
}
if (this.$root.position >= this.lyrics.lyrics[i].offset && this.$root.position < this.lyrics.lyrics[i+1].offset) return true;
return false;
},
//Get index of current lyric
currentLyric() {
if (!this.$root.audio) return 0;
return this.lyrics.lyrics.findIndex((l) => {
return this.playingNow(this.lyrics.lyrics.indexOf(l));
});
},
//Scroll to currently playing lyric
scrollLyric() {
if (!this.lyrics) return;
//Prevent janky scrolling
if (this.currentLyricIndex == this.currentLyric()) return;
this.currentLyricIndex = this.currentLyric();
//Roughly middle
let offset = window.innerHeight / 2 - 500;
this.$refs.content.scrollTo({
top: this.$refs["l"+this.currentLyricIndex][0].offsetTop + offset,
behavior: 'smooth'
});
}
},
mounted() {
this.load();
},
watch: {
songId() {
//Load on song id change
if (this.cSongId != this.songId) {
this.cSongId = this.songId;
this.load();
}
},
'$root.position'() {
this.scrollLyric();
}
}
}
</script>

View File

@ -0,0 +1,106 @@
<template>
<div>
<!-- Create playlist -->
<v-card class='text-center pa-2' v-if='!addToPlaylist'>
<v-card-text>
<p primary-title class='display-1'>Create playlist</p>
<v-text-field label='Title' class='ma-2' v-model='title'></v-text-field>
<v-textarea class='mx-2' v-model='description' label='Description' rows='1' auto-grow></v-textarea>
<v-select class='mx-2' v-model='type' :items='types' label='Type'></v-select>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn class='primary' :loading='createLoading' @click='create'>Create</v-btn>
</v-card-actions>
</v-card>
<!-- Add to playlist -->
<v-card class='text-center pa-2' v-if='addToPlaylist'>
<v-card-text>
<p primary-title class='display-1'>Add to playlist</p>
<v-btn block class='mb-1' @click='addToPlaylist = false'>
<v-icon left>mdi-playlist-plus</v-icon>
Create New
</v-btn>
<v-list>
<div v-for='playlist in playlists' :key='playlist.id'>
<v-list-item
v-if='playlist.user.id == $root.profile.id'
@click='addTrack(playlist)'
dense>
<v-list-item-avatar>
<v-img :src='playlist.image.thumb'></v-img>
</v-list-item-avatar>
<v-list-item-title>{{playlist.title}}</v-list-item-title>
</v-list-item>
</div>
<v-progress-circular indeterminate v-if='loading'></v-progress-circular>
</v-list>
</v-card-text>
</v-card>
</div>
</template>
<script>
export default {
name: 'PlaylistPopup',
data() {
return {
//Make mutable
addToPlaylist: this.track?true:false,
title: '',
description: '',
type: 'Private',
types: ['Private', 'Public'],
createLoading: false,
loading: false,
playlists: []
}
},
props: {
track: {
type: Object,
default: null
}
},
methods: {
//Create playlist
async create() {
this.createLoading = true;
await this.$axios.post('/playlist', {
description: this.description,
title: this.title,
type: this.type.toLowerCase(),
track: this.track ? this.track.id : null
});
this.createLoading = false;
this.$emit('created');
this.$emit('close');
},
//Add track to playlist
async addTrack(playlist) {
await this.$axios.post(`/playlist/${playlist.id}/tracks`, {track: this.track.id});
this.$emit('close');
}
},
async mounted() {
//Load playlists, if adding to playlist
if (this.track) {
this.loading = true;
let res = await this.$axios.get(`/library/playlists`);
this.playlists = res.data.data;
this.loading = false;
}
}
};
</script>

View File

@ -0,0 +1,166 @@
<template>
<div>
<!-- List tile -->
<v-list-item @click='click' v-if='!card'>
<v-hover v-slot:default='{hover}'>
<v-list-item-avatar>
<v-img :src='playlist.image.thumb'></v-img>
<v-overlay absolute :value='hover'>
<v-btn icon large @click.stop='play'>
<v-icon>mdi-play</v-icon>
</v-btn>
</v-overlay>
</v-list-item-avatar>
</v-hover>
<v-list-item-content>
<v-list-item-title>{{playlist.title}}</v-list-item-title>
<v-list-item-subtitle>{{$numberString(playlist.trackCount)}} tracks</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<!-- Context menu -->
<v-menu v-model='menu' offset-y offset-x absolue>
<template v-slot:activator="{on, attrs}">
<v-btn v-on='on' v-bind='attrs' icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<!-- Play -->
<v-list-item dense @click='play'>
<v-list-item-icon>
<v-icon>mdi-play</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Play</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Remove -->
<v-list-item dense v-if='canRemove' @click='remove'>
<v-list-item-icon>
<v-icon>mdi-playlist-remove</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Remove</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Download -->
<v-list-item dense @click='download'>
<v-list-item-icon>
<v-icon>mdi-download</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Download</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</v-list-item-action>
</v-list-item>
<!-- Card -->
<v-card v-if='card' max-width='175px' max-height='175px' @click='click' rounded>
<v-hover v-slot:default='{hover}'>
<div>
<v-img :src='playlist.image.thumb'>
</v-img>
<v-overlay absolute :value='hover' opacity='0.5'>
<v-btn fab small color='white' @click.stop='play'>
<v-icon color='black'>mdi-play</v-icon>
</v-btn>
</v-overlay>
</div>
</v-hover>
</v-card>
<DownloadDialog :tracks='tracks' v-if='downloadDialog' @close='downloadDialog = false'></DownloadDialog>
</div>
</template>
<script>
import DownloadDialog from '@/components/DownloadDialog.vue';
export default {
name: 'PlaylistTile',
components: {DownloadDialog},
data() {
return {
menu: false,
hover: false,
downloadDialog: false,
tracks: null
}
},
props: {
playlist: Object,
card: {
type: Boolean,
default: false
}
},
methods: {
async play() {
let playlist = this.playlist;
//Load playlist tracks
if (playlist.tracks.length != playlist.trackCount) {
let data = await this.$axios.get(`/playlist/${playlist.id}?full=iguess`);
playlist = data.data;
}
//Error handling
if (!playlist) return;
this.$root.queue.source = {
text: playlist.title,
source: 'playlist',
data: playlist.id
};
this.$root.replaceQueue(playlist.tracks);
this.$root.playIndex(0);
},
//On click navigate to details
click() {
this.$router.push({
path: '/playlist',
query: {playlist: JSON.stringify(this.playlist)}
});
},
async remove() {
//Delete own playlist
if (this.playlist.user.id == this.$root.profile.id) {
await this.$axios.delete(`/playlist/${this.playlist.id}`);
} else {
//Remove from library
await this.$axios.get('/library/playlist&id=' + this.playlist.id);
}
this.$emit('remove');
},
async download() {
let tracks = this.playlist.tracks;
if (tracks.length < this.playlist.trackCount) {
let data = await this.$axios.get(`/playlist/${this.playlist.id}?full=iguess`);
tracks = data.data.tracks;
}
this.tracks = tracks;
this.downloadDialog = true;
}
},
computed: {
canRemove() {
//Own playlist
if (this.$root.profile.id == this.playlist.user.id) return true;
return false;
}
}
};
</script>

View File

@ -0,0 +1,50 @@
<template>
<div>
<v-card max-width='175px' max-height='210px' @click='play' :loading='loading'>
<v-img :src='stl.cover.thumb'>
</v-img>
<div class='pa-2 text-subtitle-2 text-center text-truncate'>{{stl.title}}</div>
</v-card>
</div>
</template>
<script>
export default {
name: 'SmartTrackList',
props: {
stl: Object
},
data() {
return {
loading: false
}
},
methods: {
//Load stt as source
async play() {
this.loading = true;
//Load data
let res = await this.$axios.get('/smarttracklist/' + this.stl.id);
if (!res.data) {
this.loading = false;
return;
}
//Send to player
this.$root.queue.source = {
text: this.stl.title,
source: 'smarttracklist',
data: this.stl.id
};
this.$root.replaceQueue(res.data);
this.$root.playIndex(0);
this.loading = false;
}
}
}
</script>

View File

@ -0,0 +1,207 @@
<template>
<v-list-item two-line @click='$emit("click")'>
<v-list-item-avatar>
<v-img :src='track.albumArt.thumb'></v-img>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title
:class='{"primary--text": track.id == ($root.track ? $root.track : {id: null}).id}'
>{{track.title}}</v-list-item-title>
<v-list-item-subtitle>{{track.artistString}}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<!-- Quick add/remoev to library -->
<v-btn @click.stop='addLibrary' icon v-if='!isLibrary'>
<v-icon>mdi-heart</v-icon>
</v-btn>
<v-btn @click.stop='removeLibrary' icon v-if='isLibrary'>
<v-icon>mdi-heart-remove</v-icon>
</v-btn>
</v-list-item-action>
<v-list-item-action>
<!-- Quick add to playlist -->
<v-btn @click.stop='popup = true' icon>
<v-icon>mdi-playlist-plus</v-icon>
</v-btn>
</v-list-item-action>
<v-list-item-action>
<!-- Context menu -->
<v-menu v-model='menu' offset-y offset-x absolue>
<template v-slot:activator="{on, attrs}">
<v-btn v-on='on' v-bind='attrs' icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<!-- Play Next -->
<v-list-item dense @click='playNext'>
<v-list-item-icon>
<v-icon>mdi-playlist-plus</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Play next</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Add to end of queue -->
<v-list-item dense @click='addQueue'>
<v-list-item-icon>
<v-icon>mdi-playlist-plus</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Add to queue</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Add to library -->
<v-list-item dense @click='addLibrary' v-if='!isLibrary'>
<v-list-item-icon>
<v-icon>mdi-heart</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Add to library</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Remove from library -->
<v-list-item dense @click='removeLibrary' v-if='isLibrary'>
<v-list-item-icon>
<v-icon>mdi-heart-remove</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Remove from library</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Add to playlist -->
<v-list-item dense @click='popup = true' v-if='!playlistId'>
<v-list-item-icon>
<v-icon>mdi-playlist-plus</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Add to playlist</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Remove from playlist -->
<v-list-item dense @click='removePlaylist' v-if='playlistId'>
<v-list-item-icon>
<v-icon>mdi-playlist-remove</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Remove from playlist</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Go to album -->
<v-list-item dense @click='goAlbum'>
<v-list-item-icon>
<v-icon>mdi-album</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Go to "{{track.album.title}}"</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Go to artists -->
<v-list-item
dense
@click='goArtist(artist)'
v-for="artist in track.artists"
:key='"ART" + artist.id'
>
<v-list-item-icon>
<v-icon>mdi-account-music</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Go to "{{artist.name}}"</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Download -->
<v-list-item dense @click='download'>
<v-list-item-icon>
<v-icon>mdi-download</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Download</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</v-list-item-action>
<!-- Add to playlist dialog -->
<v-dialog max-width="400px" v-model='popup'>
<PlaylistPopup :track='this.track' @close='popup = false'></PlaylistPopup>
</v-dialog>
<DownloadDialog :tracks='[track]' v-if='downloadDialog' @close='downloadDialog = false'></DownloadDialog>
</v-list-item>
</template>
<script>
import PlaylistPopup from '@/components/PlaylistPopup.vue';
import DownloadDialog from '@/components/DownloadDialog.vue';
export default {
name: 'TrackTile',
components: {
PlaylistPopup, DownloadDialog
},
data() {
return {
menu: false,
popup: false,
downloadDialog: false,
isLibrary: this.$root.libraryTracks.includes(this.track.id)
}
},
props: {
track: Object,
//If specified, track can be removed
playlistId: {
type: String,
default: null
},
},
methods: {
//Add track next to queue
playNext() {
this.$root.addTrackIndex(this.track, this.$root.queueIndex+1);
},
addQueue() {
this.$root.queue.push(this.track);
},
addLibrary() {
this.isLibrary = true;
this.$root.libraryTracks.push(this.track.id);
this.$axios.put(`/library/tracks?id=${this.track.id}`);
},
goAlbum() {
this.$router.push({
path: '/album',
query: {album: JSON.stringify(this.track.album)}
});
},
goArtist(a) {
this.$router.push({
path: '/artist',
query: {artist: JSON.stringify(a)}
});
},
async removeLibrary() {
this.isLibrary = false;
this.$root.libraryTracks.splice(this.$root.libraryTracks.indexOf(this.track.id), 1);
await this.$axios.delete(`/library/tracks?id=${this.track.id}`);
this.$emit('remove');
},
//Remove from playlist
async removePlaylist() {
await this.$axios.delete(`/playlist/${this.playlistId}/tracks`, {
data: {track: this.track.id}
});
this.$emit('remove');
},
//Download track
async download() {
this.downloadDialog = true;
}
}
}
</script>