Display playlist meta

This commit is contained in:
An Phan 2016-01-14 16:02:59 +08:00
parent 2aa2f5f27e
commit eb9e9a2317
14 changed files with 365 additions and 264 deletions

View file

@ -187,6 +187,7 @@
* Load the Favorites view.
*/
loadFavorites() {
this.$broadcast('favorites:load');
this.loadMainView('favorites');
},

View file

@ -8,6 +8,12 @@
<i class="fa fa-angle-up toggler"
v-show="isPhone && showingControls"
@click.prevent="showingControls = false"></i>
<span class="meta" v-show="meta.songCount">
{{ meta.songCount }} song{{ meta.songCount === 1 ? '' : 's' }}
{{ meta.totalLength }}
</span>
</span>
<div class="buttons" v-show="!isPhone || showingControls">
@ -50,16 +56,12 @@
<script>
import isMobile from 'ismobilejs';
import songList from '../../shared/song-list.vue';
import favoriteStore from '../../../stores/favorite';
import playback from '../../../services/playback';
import shuffleSelectedMixin from '../../../mixins/shuffle-selected';
import hasAddToMenuMixin from '../../../mixins/has-add-to-menu';
import hasSongList from '../../../mixins/has-song-list';
export default {
mixins: [shuffleSelectedMixin, hasAddToMenuMixin],
components: { songList },
mixins: [hasSongList],
data () {
return {

View file

@ -88,10 +88,10 @@
h1.heading {
font-weight: $fontWeight_UltraThin;
font-size: 48px;
font-size: 36px;
padding: 12px 24px;
border-bottom: 1px solid $color2ndBgr;
min-height: 90px;
min-height: 96px;
position: relative;
align-items: center;
align-content: stretch;
@ -101,6 +101,13 @@
flex: 1;
}
.meta {
display: block;
opacity: .5;
font-size: $fontSize;
margin: 12px 0 0 2px;
}
.buttons {
text-align: right;
z-index: 2;
@ -161,6 +168,10 @@
color: $colorHighlight;
}
.meta {
display: none;
}
.buttons, input[type="search"] {
justify-content: center;
margin-top: 8px;

View file

@ -8,6 +8,12 @@
<i class="fa fa-angle-up toggler"
v-show="isPhone && showingControls"
@click.prevent="showingControls = false"></i>
<span class="meta" v-show="meta.songCount">
{{ meta.songCount }} song{{ meta.songCount === 1 ? '' : 's' }}
{{ meta.totalLength }}
</span>
</span>
<div class="buttons" v-show="!isPhone || showingControls">
@ -53,16 +59,12 @@
<script>
import isMobile from 'ismobilejs';
import songList from '../../shared/song-list.vue';
import playlistStore from '../../../stores/playlist';
import playback from '../../../services/playback';
import shuffleSelectedMixin from '../../../mixins/shuffle-selected';
import hasAddToMenuMixin from '../../../mixins/has-add-to-menu';
import hasSongList from '../../../mixins/has-song-list';
export default {
mixins: [shuffleSelectedMixin, hasAddToMenuMixin],
components: { songList },
mixins: [hasSongList],
data() {
return {

View file

@ -8,6 +8,12 @@
<i class="fa fa-angle-up toggler"
v-show="isPhone && showingControls"
@click.prevent="showingControls = false"></i>
<span class="meta" v-show="meta.songCount">
{{ meta.songCount }} song{{ meta.songCount === 1 ? '' : 's' }}
{{ meta.totalLength }}
</span>
</span>
<div class="buttons" v-show="!isPhone || showingControls">
@ -55,18 +61,14 @@
import _ from 'lodash';
import isMobile from 'ismobilejs';
import songList from '../../shared/song-list.vue';
import playlistStore from '../../../stores/playlist';
import queueStore from '../../../stores/queue';
import songStore from '../../../stores/song';
import playback from '../../../services/playback';
import shuffleSelectedMixin from '../../../mixins/shuffle-selected';
import hasAddToMenuMixin from '../../../mixins/has-add-to-menu';
import hasSongList from '../../../mixins/has-song-list';
export default {
mixins: [shuffleSelectedMixin, hasAddToMenuMixin],
components: { songList },
mixins: [hasSongList],
data() {
return {

View file

@ -8,6 +8,12 @@
<i class="fa fa-angle-up toggler"
v-show="isPhone && showingControls"
@click.prevent="showingControls = false"></i>
<span class="meta" v-show="meta.songCount">
{{ meta.songCount }} song{{ meta.songCount === 1 ? '' : 's' }}
{{ meta.totalLength }}
</span>
</span>
<div class="buttons" v-show="!isPhone || showingControls">
@ -44,16 +50,12 @@
<script>
import isMobile from 'ismobilejs';
import songList from '../../shared/song-list.vue';
import songStore from '../../../stores/song';
import playback from '../../../services/playback';
import shuffleSelectedMixin from '../../../mixins/shuffle-selected';
import hasAddToMenuMixin from '../../../mixins/has-add-to-menu';
import hasSongList from '../../../mixins/has-song-list';
export default {
mixins: [shuffleSelectedMixin, hasAddToMenuMixin],
components: { songList },
mixins: [hasSongList],
data() {
return {

View file

@ -0,0 +1,216 @@
<template>
<li @dblclick.prevent="edit" class="playlist" :class="[type, editing ? 'editing' : '']">
<a @click.prevent="load"
@dragleave="removeDroppableState"
@dragover.prevent="allowDrop"
@drop.stop="handleDrop($event)"
:class="{ 'active': active }"
>{{ playlist.name }}</a>
<input type="text"
@keyup.esc="cancelEdit"
@keyup.enter="update"
@blur="update"
v-model="playlist.name"
v-koel-focus="editing"
required
>
</li>
</template>
<script>
import $ from 'jquery';
import songStore from '../../../stores/song';
import playlistStore from '../../../stores/playlist';
import favoriteStore from '../../../stores/favorite';
export default {
props: ['playlist', 'type'],
data() {
return {
newName: '',
editing: false,
active: false,
};
},
computed: {
/**
* Determine if the current menu item is the "Favorites" one.
*
* @return {Boolean}
*/
isFavorites() {
return this.type === 'favorites';
},
},
methods: {
/**
* Load songs in the current playlist.
*/
load() {
if (this.isFavorites) {
this.$root.loadFavorites();
} else {
this.$root.loadPlaylist(this.playlist);
}
},
/**
* Show the form to edit the playlist.
*/
edit() {
if (this.isFavorites) {
return;
}
this.beforeEditCache = this.playlist.name;
this.editing = true;
},
/**
* Update the playlist's name.
*/
update() {
if (this.isFavorites || !this.editing) {
return;
}
this.editing = false;
this.playlist.name = this.playlist.name.trim();
if (!this.playlist.name) {
this.playlist.name = this.beforeEditCache;
return;
}
playlistStore.update(this.playlist);
},
/**
* Cancel editing.
*/
cancelEdit() {
this.editing = false;
this.playlist.name = this.beforeEditCache;
},
/**
* Remove the droppable state when a dragleave event occurs on the playlist's DOM element.
*
* @param {Object} e The dragleave event.
*/
removeDroppableState(e) {
$(e.target).removeClass('droppable');
},
/**
* Add a "droppable" class and set the drop effect when an item is dragged over the playlist's
* DOM element.
*
* @param object e The dragover event.
*/
allowDrop(e) {
$(e.target).addClass('droppable');
e.dataTransfer.dropEffect = 'move';
return false;
},
/**
* Handle songs dropped to our favorite or playlist menu item.
*
* @param {Object} e The event
*
* @return {boolean}
*/
handleDrop(e) {
this.removeDroppableState(e);
if (!e.dataTransfer.getData('text/plain')) {
return false;
}
var songs = songStore.byIds(e.dataTransfer.getData('text/plain').split(','));
if (!songs.length) {
return false;
}
if (this.type === 'favorites') {
favoriteStore.like(songs);
} else {
playlistStore.addSongs(this.playlist, songs);
}
return false;
},
},
events: {
/**
* Listen to 'playlist:load' event to determine if the current item should be highlighted.
*
* @param {Object} playlist The playlist being loaded.
*/
'playlist:load': function (playlist) {
this.active = this.playlist === playlist;
},
/**
* Listen to 'favorites:load' event to highlight the Favorites item if need be.
*/
'favorites:load': function () {
this.active = this.isFavorites;
},
},
};
</script>
<style lang="sass" scoped>
@import "resources/assets/sass/partials/_vars.scss";
@import "resources/assets/sass/partials/_mixins.scss";
.playlist {
user-select: none;
a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
span {
pointer-events: none;
}
&::before {
content: "\f0f6";
}
}
&.favorites a::before {
content: "\f004";
color: $colorHeart;
}
input {
display: none;
width: calc(100% - 32px);
margin: 5px 16px;
}
&.editing {
a {
display: none !important;
}
input {
display: block;
}
}
}
</style>

View file

@ -13,80 +13,38 @@
</form>
<ul class="menu">
<li>
<a class="favorites"
@click.prevent="loadFavorites"
:class="[currentView == 'favorites' ? 'active' : '']"
@dragleave="removeDroppableState"
@dragover.prevent="allowDrop"
@drop.stop="handleDrop(null, $event)">Favorites</a>
</li>
<li v-for="p in state.playlists"
@dblclick.prevent="edit(p)"
class="playlist"
:class="{ editing: p == editedPlaylist }"
>
<a @click.prevent="load(p)"
@dragleave="removeDroppableState"
@dragover.prevent="allowDrop"
@drop.stop="handleDrop(p, $event)"
:class="[(currentView == 'playlist' && currentPlaylist == p) ? 'active' : '']"
>
{{ p.name }}
</a>
<input type="text"
@keyup.esc="cancelEdit(p)"
@keyup.enter="update(p)"
@blur="update(p)"
v-model="p.name"
v-koel-focus="p == editedPlaylist"
required
>
</li>
<playlist-item
type="favorites"
:playlist="{ name: 'Favorites', songs: favoriteState.songs }"></playlist-item>
<playlist-item
v-for="playlist in playlistState.playlists"
type="playlist"
:playlist="playlist"></playlist-item>
</ul>
</section>
</template>
<script>
import songStore from '../../../stores/song';
import playlistStore from '../../../stores/playlist';
import favoriteStore from '../../../stores/favorite';
import $ from 'jquery';
import playlistItem from './playlist-item.vue';
export default {
props: ['currentView'],
components: { playlistItem },
data() {
return {
state: playlistStore.state,
playlistState: playlistStore.state,
favoriteState: favoriteStore.state,
creating: false,
newName: '',
currentPlaylist: null,
editedPlaylist: playlistStore.stub,
};
},
methods: {
/**
* Load a playlist.
*
* @param {Object} p The playlist.
*/
load(p) {
this.$root.loadPlaylist(p);
},
/**
* Load the Favorite playlist.
*/
loadFavorites() {
this.currentPlaylist = null;
this.$root.loadFavorites();
},
/**
* Store/create a new playlist.
*/
@ -95,101 +53,6 @@
playlistStore.store(this.newName, [], () => this.newName = '');
},
/**
* Show the form to edit a playlist.
*
* @param {Object} p The playlist
*/
edit(p) {
this.beforeEditCache = p.name;
this.editedPlaylist = p;
},
/**
* Update a playlist's name.
*
* @param {Object} p The playlist
*/
update(p) {
if (!this.editedPlaylist) {
return;
}
this.editedPlaylist = null;
p.name = p.name.trim();
if (!p.name) {
p.name = this.beforeEditCache;
return;
}
playlistStore.update(p);
},
/**
* Cancel editing the currently edited playlist.
*
* @param {Object} p The playlist.
*/
cancelEdit(p) {
this.editedPlaylist = null;
p.name = this.beforeEditCache;
},
/**
* Remove the droppable state when a dragleave event occurs on the playlist's DOM element.
*
* @param {Object} e The dragleave event.
*/
removeDroppableState(e) {
$(e.target).removeClass('droppable');
},
/**
* Add a "droppable" class and set the drop effect when an item is dragged over the playlist's
* DOM element.
*
* @param object e The dragover event.
*/
allowDrop(e) {
$(e.target).addClass('droppable');
e.dataTransfer.dropEffect = 'move';
return false;
},
/**
* Handle songs dropped to our favorite or playlist menu item.
*
* @param {?Object} playlist The playlist object, or null if dropping to Favorites.
* @param {Object} e The event
*
* @return {boolean}
*/
handleDrop(playlist, e) {
this.removeDroppableState(e);
var songs = songStore.byIds(e.dataTransfer.getData('text/plain').split(','));
if (!songs.length) {
return false;
}
if (playlist) {
playlistStore.addSongs(playlist, songs);
} else {
favoriteStore.like(songs);
}
return false;
},
},
events: {
'playlist:load': function (p) {
this.currentPlaylist = p;
},
},
};
</script>
@ -199,44 +62,6 @@
@import "resources/assets/sass/partials/_mixins.scss";
#playlists {
.menu {
a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
a::before {
content: "\f0f6";
}
a.favorites::before {
content: "\f004";
color: $colorHeart;
}
.playlist {
user-select: none;
input {
display: none;
width: calc(100% - 32px);
margin: 5px 16px;
}
&.editing {
a {
display: none;
}
input {
display: block;
}
}
}
}
form.create {
padding: 8px 16px;

View file

@ -79,12 +79,19 @@
watch: {
/**
* Watch the items, so that we can always make sure a queue is not sorted in any ways.
* Watch the items.
*/
items() {
// Make sure a queue is not sorted in any ways.
if (this.type === 'queue') {
this.sortKey = '';
}
// Dispatch this event for the parent to update the song count and duration status.
this.$dispatch('songlist:changed', {
songCount: this.items.length,
totalLength: songStore.getLength(this.items, true),
});
},
},

View file

@ -1,21 +0,0 @@
/**
* Add necessary functionalities into a (song-list type) component.
*/
import addToMenu from '../components/shared/add-to-menu.vue';
export default {
components: { addToMenu },
data() {
return {
showingAddToMenu: false,
};
},
events: {
'add-to-menu:close': function () {
this.showingAddToMenu = false;
},
},
};

View file

@ -0,0 +1,62 @@
/**
* Add necessary functionalities into a view that contains a song-list component.
*/
import _ from 'lodash';
import playback from '../services/playback';
import addToMenu from '../components/shared/add-to-menu.vue';
import songList from '../components/shared/song-list.vue';
export default {
components: { addToMenu, songList },
data() {
return {
/**
* Whether or not to show the "Add To" button in the header.
*
* @type {Boolean}
*/
showingAddToMenu: false,
/**
* An array of selected songs in the list.
*
* @type {Array}
*/
selectedSongs: [],
meta: {
songCount: 0,
totalLength: '00:00',
},
};
},
methods: {
/**
* Shuffles the currently selected songs.
*/
shuffleSelected() {
if (this.selectedSongs.length < 2) {
return;
}
playback.queueAndPlay(this.selectedSongs, true);
},
},
events: {
/**
* Listen to add-to-menu:close event to set showingAddToMenu to false (and subsequently close the menu).
*/
'add-to-menu:close': function () {
this.showingAddToMenu = false;
},
'songlist:changed': function (meta) {
this.meta = _.assign(this.meta, meta);
},
},
};

View file

@ -1,28 +0,0 @@
import $ from 'jquery';
import playback from '../services/playback';
/**
* Add a "shuffle selected" functionality to any component containing a song-list component using this mixin.
* Such a component should:
* - pass "selectedSongs" SYNC prop into the song-list component, e.g.
* <song-list :items="state.songs" :selected-songs.sync="selectedSongs" type="queue"></song-list>
* - trigger shuffling with shuffleSelected() method
*/
export default {
data() {
return {
selectedSongs: [],
};
},
methods: {
shuffleSelected() {
if (this.selectedSongs.length < 2) {
return;
}
playback.queueAndPlay(this.selectedSongs, true);
},
},
};

View file

@ -6,6 +6,8 @@ import utils from '../services/utils';
export default {
state: {
songs: [],
length: 0,
fmtLength: '',
},
all() {

View file

@ -58,6 +58,24 @@ export default {
});
},
/**
* Get the total duration of some songs.
*
* @param {Array} songs
* @param {boolean} toHis Wheter to convert the duration into H:i:s format
*
* @return {float|string}
*/
getLength(songs, toHis) {
var duration = _.reduce(songs, (length, song) => length + song.length, 0);
if (toHis) {
return utils.secondsToHis(duration);
}
return duration;
},
/**
* Get all songs.
*