Add the ability to share song URLs

This commit is contained in:
An Phan 2016-07-07 21:54:20 +08:00
parent ae3d2fadf3
commit 56045ef06c
No known key found for this signature in database
GPG key ID: 05536BB4BCDC02A2
8 changed files with 108 additions and 20 deletions

View file

@ -41,6 +41,7 @@
"postcss-cssnext": "^2.6.0", "postcss-cssnext": "^2.6.0",
"rangeslider.js": "^2.1.1", "rangeslider.js": "^2.1.1",
"rangetouch": "0.0.9", "rangetouch": "0.0.9",
"select": "^1.0.6",
"sinon": "^1.17.2", "sinon": "^1.17.2",
"vue": "^2.0.0-alpha.8", "vue": "^2.0.0-alpha.8",
"vue-hot-reload-api": "^1.3.2", "vue-hot-reload-api": "^1.3.2",

View file

@ -33,7 +33,7 @@ import overlay from './components/shared/overlay.vue';
import loginForm from './components/auth/login-form.vue'; import loginForm from './components/auth/login-form.vue';
import editSongsForm from './components/modals/edit-songs-form.vue'; import editSongsForm from './components/modals/edit-songs-form.vue';
import { event, showOverlay, hideOverlay, loadMainView, forceReloadWindow } from './utils'; import { event, showOverlay, hideOverlay, loadMainView, forceReloadWindow, url } from './utils';
import { sharedStore, queueStore, songStore, userStore, preferenceStore as preferences } from './stores'; import { sharedStore, queueStore, songStore, userStore, preferenceStore as preferences } from './stores';
import { playback, ls } from './services'; import { playback, ls } from './services';
import { focusDirective, clickawayDirective } from './directives'; import { focusDirective, clickawayDirective } from './directives';
@ -58,6 +58,9 @@ export default {
// Create the element to be the ghost drag image. // Create the element to be the ghost drag image.
$('<div id="dragGhost"></div>').appendTo('body'); $('<div id="dragGhost"></div>').appendTo('body');
// And the textarea to copy stuff
$('<textarea id="copyArea"></textarea>').appendTo('body');
// Add an ugly mac/non-mac class for OS-targeting styles. // Add an ugly mac/non-mac class for OS-targeting styles.
// I'm crying inside. // I'm crying inside.
$('html').addClass(navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : 'non-mac'); $('html').addClass(navigator.userAgent.indexOf('Mac') !== -1 ? 'mac' : 'non-mac');
@ -190,6 +193,17 @@ export default {
forceReloadWindow(); forceReloadWindow();
}); });
}, },
/**
* Parse song ID from permalink and play.
*/
'koel:ready': () => {
const songId = url.parseSongId();
if (!songId) return;
const song = songStore.byId(songId);
if (!song) return;
playback.queueAndPlay(song);
},
}); });
}, },
}; };
@ -224,6 +238,17 @@ Vue.directive('koel-clickaway',clickawayDirective);
} }
} }
#copyArea {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
html.touchevents & {
display: none;
}
}
#main, .login-wrapper { #main, .login-wrapper {
display: flex; display: flex;
min-height: 100vh; min-height: 100vh;

View file

@ -3,16 +3,14 @@
@blur="close" @blur="close"
:style="{ top: top + 'px', left: left + 'px' }" :style="{ top: top + 'px', left: left + 'px' }"
> >
<li v-if="onlyOneSongSelected" @click="doPlayback"> <template v-show="onlyOneSongSelected">
<span v-if="songs[0].playbackState !== 'playing'">Play</span> <li @click="doPlayback">
<span v-if="!firstSongPlaying">Play</span>
<span v-else>Pause</span> <span v-else>Pause</span>
</li> </li>
<li v-if="onlyOneSongSelected" @click="viewAlbumDetails(songs[0].album)"> <li @click="viewAlbumDetails(songs[0].album)">Go to Album</li>
<span>Go to Album</span> <li @click="viewArtistDetails(songs[0].artist)">Go to Artist</li>
</li> </template>
<li v-if="onlyOneSongSelected" @click="viewArtistDetails(songs[0].artist)">
<span>Go to Artist</span>
</li>
<li class="has-sub">Add To <li class="has-sub">Add To
<ul class="menu submenu"> <ul class="menu submenu">
<li @click="queueSongsAfterCurrent">After Current Song</li> <li @click="queueSongsAfterCurrent">After Current Song</li>
@ -20,14 +18,14 @@
<li @click="queueSongsToTop">Top of Queue</li> <li @click="queueSongsToTop">Top of Queue</li>
<li class="separator"></li> <li class="separator"></li>
<li @click="addSongsToFavorite">Favorites</li> <li @click="addSongsToFavorite">Favorites</li>
<li class="separator" v-show="playlistState.playlists.length"></li> <li class="separator" v-if="playlistState.playlists.length"></li>
<li v-for="playlist in playlistState.playlists" <li v-for="p in playlistState.playlists" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li>
@click="addSongsToExistingPlaylist(playlist)"
>{{ playlist.name }}</li>
</ul> </ul>
</li> </li>
<li v-if="isAdmin" @click="openEditForm">Edit</li> <li v-if="isAdmin" @click="openEditForm">Edit</li>
<li @click="download" v-if="sharedState.allowDownload">Download</li> <li v-if="sharedState.allowDownload" @click="download">Download</li>
<!-- somehow v-if doesn't work here -->
<li v-show="copyable && onlyOneSongSelected" @click="copyUrl">Copy Shareable URL</li>
</ul> </ul>
</template> </template>
@ -37,8 +35,8 @@ import $ from 'jquery';
import songMenuMethods from '../../mixins/song-menu-methods'; import songMenuMethods from '../../mixins/song-menu-methods';
import artistAlbumDetails from '../../mixins/artist-album-details'; import artistAlbumDetails from '../../mixins/artist-album-details';
import { event } from '../../utils'; import { event, isClipboardSupported, copyText } from '../../utils';
import { sharedStore, queueStore, userStore, playlistStore } from '../../stores'; import { sharedStore, songStore, queueStore, userStore, playlistStore } from '../../stores';
import { playback, download } from '../../services'; import { playback, download } from '../../services';
export default { export default {
@ -50,6 +48,7 @@ export default {
return { return {
playlistState: playlistStore.state, playlistState: playlistStore.state,
sharedState: sharedStore.state, sharedState: sharedStore.state,
copyable: isClipboardSupported(),
}; };
}, },
@ -58,6 +57,10 @@ export default {
return this.songs.length === 1; return this.songs.length === 1;
}, },
firstSongPlaying() {
return this.songs[0] ? this.songs[0].playbackState === 'playing' : false;
},
isAdmin() { isAdmin() {
return userStore.current.is_admin; return userStore.current.is_admin;
}, },
@ -129,6 +132,10 @@ export default {
download.fromSongs(this.songs); download.fromSongs(this.songs);
this.close(); this.close();
}, },
copyUrl() {
copyText(songStore.getShareableUrl(this.songs[0]));
},
}, },
/** /**

View file

@ -313,6 +313,18 @@ export const songStore = {
return `${sharedStore.state.cdnUrl}api/${song.id}/play?jwt-token=${ls.get('jwt-token')}`; return `${sharedStore.state.cdnUrl}api/${song.id}/play?jwt-token=${ls.get('jwt-token')}`;
}, },
/**
* Get a song's shareable URL.
* Visiting this URL will automatically queue the song and play it.
*
* @param {Object} song
*
* @return {string}
*/
getShareableUrl(song) {
return `${window.location.origin}/#!/song/${song.id}`;
},
/** /**
* Get the last n recently played songs. * Get the last n recently played songs.
* *

View file

@ -1,7 +1,7 @@
/** /**
* Other common methods. * Other common methods.
*/ */
import select from 'select';
import { event } from '../utils' import { event } from '../utils'
/** /**
@ -68,9 +68,22 @@ export function showOverlay(message = 'Just a little patience…', type = 'loadi
}; };
/** /**
* Hide the overlay * Hide the overlay.
* @return {[type]} [description] * @return {[type]} [description]
*/ */
export function hideOverlay() { export function hideOverlay() {
event.emit('overlay:hide'); event.emit('overlay:hide');
}; };
/**
* Copy a text into clipboard.
*
* @param {string} txt
*/
export function copyText(txt) {
const copyArea = document.querySelector('#copyArea');
copyArea.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
copyArea.value = txt;
select(copyArea);
document.execCommand('copy');
};

View file

@ -2,3 +2,4 @@ export * from './filters';
export * from './formatters'; export * from './formatters';
export * from './supports'; export * from './supports';
export * from './common'; export * from './common';
export * from './url';

View file

@ -31,6 +31,14 @@ export function isAudioContextSupported() {
return true; return true;
}; };
/**
* Checks if HTML5 clipboard can be used.
*
* @return {Boolean}
*/
export function isClipboardSupported() {
return 'execCommand' in document;
};
const event = { const event = {
bus: null, bus: null,

View file

@ -0,0 +1,21 @@
/**
* URL-related helpers
* @type {Object}
*/
export const url = {
/**
* Parse the song ID from a hash.
*
* @param {string} hash
*
* @return {string|boolean}
*/
parseSongId(hash = null) {
if (!hash) {
hash = window.location.hash;
}
const matches = hash.match(/#!\/song\/([a-f0-9]{32}$)/);
return matches ? matches[1] : false;
},
};