mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
Add the ability to share song URLs
This commit is contained in:
parent
ae3d2fadf3
commit
56045ef06c
8 changed files with 108 additions and 20 deletions
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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-else>Pause</span>
|
<span v-if="!firstSongPlaying">Play</span>
|
||||||
</li>
|
<span v-else>Pause</span>
|
||||||
<li v-if="onlyOneSongSelected" @click="viewAlbumDetails(songs[0].album)">
|
</li>
|
||||||
<span>Go to Album</span>
|
<li @click="viewAlbumDetails(songs[0].album)">Go to Album</li>
|
||||||
</li>
|
<li @click="viewArtistDetails(songs[0].artist)">Go to Artist</li>
|
||||||
<li v-if="onlyOneSongSelected" @click="viewArtistDetails(songs[0].artist)">
|
</template>
|
||||||
<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]));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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');
|
||||||
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
|
21
resources/assets/js/utils/url.js
Normal file
21
resources/assets/js/utils/url.js
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in a new issue