mirror of
https://github.com/owntone/owntone-server.git
synced 2025-07-28 17:51:00 -04:00
Merge pull request #1082 from chme/web_next
Player web interface v0.8.0
This commit is contained in:
commit
4331153966
@ -10,10 +10,10 @@
|
|||||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css"> -->
|
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css"> -->
|
||||||
|
|
||||||
<!-- Local libraries -->
|
<!-- Local libraries -->
|
||||||
<link rel="stylesheet" href="/admin/vendor/fontawesome/css/all.min.css">
|
<link rel="stylesheet" href="admin/vendor/fontawesome/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/admin/vendor/bulma/bulma.min.css">
|
<link rel="stylesheet" href="admin/vendor/bulma/bulma.min.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/admin/css/forked-daapd.css">
|
<link rel="stylesheet" href="admin/css/forked-daapd.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
-->
|
-->
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item has-text-black" href="/">forked-daapd</a>
|
<a class="navbar-item has-text-black" href="./">forked-daapd</a>
|
||||||
<a class="navbar-item" href="https://github.com/ejurgensen/forked-daapd" title="GitHub"><i class="fab fa-github"></i></a>
|
<a class="navbar-item" href="https://github.com/ejurgensen/forked-daapd" title="GitHub"><i class="fab fa-github"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -281,9 +281,9 @@
|
|||||||
<!-- <script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script> -->
|
<!-- <script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script> -->
|
||||||
|
|
||||||
<!-- Local libraries -->
|
<!-- Local libraries -->
|
||||||
<script src="/admin/vendor/vue/vue.min.js"></script>
|
<script src="admin/vendor/vue/vue.min.js"></script>
|
||||||
<script src="/admin/vendor/axios/axios.min.js"></script>
|
<script src="admin/vendor/axios/axios.min.js"></script>
|
||||||
<script src="/admin/js/forked-daapd.js"></script>
|
<script src="admin/js/forked-daapd.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -35,43 +35,43 @@ var app = new Vue({
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
loadConfig: function() {
|
loadConfig: function() {
|
||||||
axios.get('/api/config').then(response => {
|
axios.get('./api/config').then(response => {
|
||||||
this.config = response.data;
|
this.config = response.data;
|
||||||
this.connect()});
|
this.connect()});
|
||||||
},
|
},
|
||||||
|
|
||||||
loadLibrary: function() {
|
loadLibrary: function() {
|
||||||
axios.get('/api/library').then(response => this.library = response.data);
|
axios.get('./api/library').then(response => this.library = response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
loadOutputs: function() {
|
loadOutputs: function() {
|
||||||
axios.get('/api/outputs').then(response => this.outputs = response.data.outputs);
|
axios.get('./api/outputs').then(response => this.outputs = response.data.outputs);
|
||||||
},
|
},
|
||||||
|
|
||||||
loadSpotify: function() {
|
loadSpotify: function() {
|
||||||
axios.get('/api/spotify').then(response => this.spotify = response.data);
|
axios.get('./api/spotify').then(response => this.spotify = response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
loadPairing: function() {
|
loadPairing: function() {
|
||||||
axios.get('/api/pairing').then(response => this.pairing = response.data);
|
axios.get('./api/pairing').then(response => this.pairing = response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
loadLastfm: function() {
|
loadLastfm: function() {
|
||||||
axios.get('/api/lastfm').then(response => this.lastfm = response.data);
|
axios.get('./api/lastfm').then(response => this.lastfm = response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
update: function() {
|
update: function() {
|
||||||
this.library.updating = true;
|
this.library.updating = true;
|
||||||
axios.put('/api/update').then(console.log('Library is updating'));
|
axios.put('./api/update').then(console.log('Library is updating'));
|
||||||
},
|
},
|
||||||
|
|
||||||
update_meta: function() {
|
update_meta: function() {
|
||||||
this.library.updating = true;
|
this.library.updating = true;
|
||||||
axios.put('/api/rescan').then(console.log('Library is rescanning meta'));
|
axios.put('./api/rescan').then(console.log('Library is rescanning meta'));
|
||||||
},
|
},
|
||||||
|
|
||||||
kickoffPairing: function() {
|
kickoffPairing: function() {
|
||||||
axios.post('/api/pairing', this.pairing_req).then(response => {
|
axios.post('./api/pairing', this.pairing_req).then(response => {
|
||||||
console.log('Kicked off pairing');
|
console.log('Kicked off pairing');
|
||||||
if (!this.config.websocket_port) {
|
if (!this.config.websocket_port) {
|
||||||
this.pairing = {};
|
this.pairing = {};
|
||||||
@ -80,7 +80,7 @@ var app = new Vue({
|
|||||||
},
|
},
|
||||||
|
|
||||||
kickoffVerification: function() {
|
kickoffVerification: function() {
|
||||||
axios.post('/api/verification', this.verification_req).then(response => {
|
axios.post('./api/verification', this.verification_req).then(response => {
|
||||||
console.log('Kicked off verification');
|
console.log('Kicked off verification');
|
||||||
this.verification_req.pin = '';
|
this.verification_req.pin = '';
|
||||||
});
|
});
|
||||||
@ -94,7 +94,7 @@ var app = new Vue({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.put('/api/outputs/set', { outputs: selected_outputs }).then(response => {
|
axios.put('./api/outputs/set', { outputs: selected_outputs }).then(response => {
|
||||||
if (!this.config.websocket_port) {
|
if (!this.config.websocket_port) {
|
||||||
this.loadOutputs();
|
this.loadOutputs();
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ var app = new Vue({
|
|||||||
},
|
},
|
||||||
|
|
||||||
loginLibspotify: function() {
|
loginLibspotify: function() {
|
||||||
axios.post('/api/spotify-login', this.libspotify).then(response => {
|
axios.post('./api/spotify-login', this.libspotify).then(response => {
|
||||||
this.libspotify.user = '';
|
this.libspotify.user = '';
|
||||||
this.libspotify.password = '';
|
this.libspotify.password = '';
|
||||||
this.libspotify.errors.user = '';
|
this.libspotify.errors.user = '';
|
||||||
@ -117,7 +117,7 @@ var app = new Vue({
|
|||||||
},
|
},
|
||||||
|
|
||||||
loginLastfm: function() {
|
loginLastfm: function() {
|
||||||
axios.post('/api/lastfm-login', this.lastfm_login).then(response => {
|
axios.post('./api/lastfm-login', this.lastfm_login).then(response => {
|
||||||
this.lastfm_login.user = '';
|
this.lastfm_login.user = '';
|
||||||
this.lastfm_login.password = '';
|
this.lastfm_login.password = '';
|
||||||
this.lastfm_login.errors.user = '';
|
this.lastfm_login.errors.user = '';
|
||||||
@ -132,7 +132,7 @@ var app = new Vue({
|
|||||||
},
|
},
|
||||||
|
|
||||||
logoutLastfm: function() {
|
logoutLastfm: function() {
|
||||||
axios.get('/api/lastfm-logout', this.lastfm_login).then(response => {
|
axios.get('./api/lastfm-logout', this.lastfm_login).then(response => {
|
||||||
if (!this.config.websocket_port) {
|
if (!this.config.websocket_port) {
|
||||||
this.loadLastfm();
|
this.loadLastfm();
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
<!DOCTYPE html><html class="has-navbar-fixed-top has-navbar-fixed-bottom"><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>forked-daapd-web 2</title><link rel=apple-touch-icon sizes=120x120 href=/apple-touch-icon.png?ver1.1><link rel=icon type=image/png sizes=32x32 href=/favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=/favicon-16x16.png><link rel=manifest href=/site.webmanifest><link rel=mask-icon href=/safari-pinned-tab.svg color=#5bbad5><meta name=msapplication-TileColor content=#da532c><meta name=theme-color content=#ffffff><link href=/player/css/app.css rel=preload as=style><link href=/player/css/chunk-vendors.css rel=preload as=style><link href=/player/js/app.js rel=modulepreload as=script><link href=/player/js/chunk-vendors.js rel=modulepreload as=script><link href=/player/css/chunk-vendors.css rel=stylesheet><link href=/player/css/app.css rel=stylesheet></head><body><div id=app></div><script type=module src=/player/js/chunk-vendors.js></script><script type=module src=/player/js/app.js></script><script>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script><script src=/player/js/chunk-vendors-legacy.js nomodule></script><script src=/player/js/app-legacy.js nomodule></script></body></html>
|
<!DOCTYPE html><html class="has-navbar-fixed-top has-navbar-fixed-bottom"><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>forked-daapd-web 2</title><link rel=apple-touch-icon sizes=120x120 href=apple-touch-icon.png?ver1.1><link rel=icon type=image/png sizes=32x32 href=favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=favicon-16x16.png><link rel=manifest href=site.webmanifest><link rel=mask-icon href=safari-pinned-tab.svg color=#5bbad5><meta name=msapplication-TileColor content=#da532c><meta name=theme-color content=#ffffff><link href=player/css/app.css rel=preload as=style><link href=player/css/chunk-vendors.css rel=preload as=style><link href=player/js/app.js rel=modulepreload as=script><link href=player/js/chunk-vendors.js rel=modulepreload as=script><link href=player/css/chunk-vendors.css rel=stylesheet><link href=player/css/app.css rel=stylesheet></head><body><div id=app></div><script type=module src=player/js/chunk-vendors.js></script><script type=module src=player/js/app.js></script><script>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script><script src=player/js/chunk-vendors-legacy.js nomodule></script><script src=player/js/app-legacy.js nomodule></script></body></html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
20
src/httpd.c
20
src/httpd.c
@ -301,7 +301,7 @@ httpd_request_etag_matches(struct evhttp_request *req, const char *etag)
|
|||||||
|
|
||||||
// Add cache headers to allow client side caching
|
// Add cache headers to allow client side caching
|
||||||
output_headers = evhttp_request_get_output_headers(req);
|
output_headers = evhttp_request_get_output_headers(req);
|
||||||
evhttp_add_header(output_headers, "Cache-Control", "private no-cache");
|
evhttp_add_header(output_headers, "Cache-Control", "private,no-cache,max-age=0");
|
||||||
evhttp_add_header(output_headers, "ETag", etag);
|
evhttp_add_header(output_headers, "ETag", etag);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -338,12 +338,28 @@ httpd_request_not_modified_since(struct evhttp_request *req, time_t mtime)
|
|||||||
|
|
||||||
// Add cache headers to allow client side caching
|
// Add cache headers to allow client side caching
|
||||||
output_headers = evhttp_request_get_output_headers(req);
|
output_headers = evhttp_request_get_output_headers(req);
|
||||||
evhttp_add_header(output_headers, "Cache-Control", "private no-cache");
|
evhttp_add_header(output_headers, "Cache-Control", "private,no-cache,max-age=0");
|
||||||
evhttp_add_header(output_headers, "Last-Modified", last_modified);
|
evhttp_add_header(output_headers, "Last-Modified", last_modified);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
httpd_response_not_cachable(struct evhttp_request *req)
|
||||||
|
{
|
||||||
|
struct evkeyvalq *output_headers;
|
||||||
|
|
||||||
|
output_headers = evhttp_request_get_output_headers(req);
|
||||||
|
|
||||||
|
// Remove potentially set cache control headers
|
||||||
|
evhttp_remove_header(output_headers, "Cache-Control");
|
||||||
|
evhttp_remove_header(output_headers, "Last-Modified");
|
||||||
|
evhttp_remove_header(output_headers, "ETag");
|
||||||
|
|
||||||
|
// Tell clients that they are not allowed to cache this response
|
||||||
|
evhttp_add_header(output_headers, "Cache-Control", "no-store");
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
serve_file(struct evhttp_request *req, const char *uri)
|
serve_file(struct evhttp_request *req, const char *uri)
|
||||||
{
|
{
|
||||||
|
@ -107,6 +107,9 @@ httpd_request_not_modified_since(struct evhttp_request *req, time_t mtime);
|
|||||||
bool
|
bool
|
||||||
httpd_request_etag_matches(struct evhttp_request *req, const char *etag);
|
httpd_request_etag_matches(struct evhttp_request *req, const char *etag);
|
||||||
|
|
||||||
|
void
|
||||||
|
httpd_response_not_cachable(struct evhttp_request *req);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Gzips an evbuffer
|
* Gzips an evbuffer
|
||||||
*
|
*
|
||||||
|
@ -213,7 +213,7 @@ artist_to_json(struct db_group_info *dbgri)
|
|||||||
if (ret < sizeof(uri))
|
if (ret < sizeof(uri))
|
||||||
json_object_object_add(item, "uri", json_object_new_string(uri));
|
json_object_object_add(item, "uri", json_object_new_string(uri));
|
||||||
|
|
||||||
ret = snprintf(artwork_url, sizeof(artwork_url), "/artwork/group/%s", dbgri->id);
|
ret = snprintf(artwork_url, sizeof(artwork_url), "./artwork/group/%s", dbgri->id);
|
||||||
if (ret < sizeof(artwork_url))
|
if (ret < sizeof(artwork_url))
|
||||||
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
||||||
|
|
||||||
@ -261,7 +261,7 @@ album_to_json(struct db_group_info *dbgri)
|
|||||||
if (ret < sizeof(uri))
|
if (ret < sizeof(uri))
|
||||||
json_object_object_add(item, "uri", json_object_new_string(uri));
|
json_object_object_add(item, "uri", json_object_new_string(uri));
|
||||||
|
|
||||||
ret = snprintf(artwork_url, sizeof(artwork_url), "/artwork/group/%s", dbgri->id);
|
ret = snprintf(artwork_url, sizeof(artwork_url), "./artwork/group/%s", dbgri->id);
|
||||||
if (ret < sizeof(artwork_url))
|
if (ret < sizeof(artwork_url))
|
||||||
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
||||||
|
|
||||||
@ -338,6 +338,7 @@ playlist_to_json(struct db_playlist_info *dbpli)
|
|||||||
json_object *item;
|
json_object *item;
|
||||||
char uri[100];
|
char uri[100];
|
||||||
int intval;
|
int intval;
|
||||||
|
bool boolval;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
item = json_object_new_object();
|
item = json_object_new_object();
|
||||||
@ -351,6 +352,10 @@ playlist_to_json(struct db_playlist_info *dbpli)
|
|||||||
{
|
{
|
||||||
safe_json_add_string(item, "type", db_pl_type_label(intval));
|
safe_json_add_string(item, "type", db_pl_type_label(intval));
|
||||||
json_object_object_add(item, "smart_playlist", json_object_new_boolean(intval == PL_SMART));
|
json_object_object_add(item, "smart_playlist", json_object_new_boolean(intval == PL_SMART));
|
||||||
|
|
||||||
|
boolval = dbpli->query_order && strcasestr(dbpli->query_order, "random");
|
||||||
|
json_object_object_add(item, "random", json_object_new_boolean(boolval));
|
||||||
|
|
||||||
json_object_object_add(item, "folder", json_object_new_boolean(intval == PL_FOLDER));
|
json_object_object_add(item, "folder", json_object_new_boolean(intval == PL_FOLDER));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2099,7 +2104,7 @@ jsonapi_reply_player(struct httpd_request *hreq)
|
|||||||
json_object_object_add(reply, "item_id", json_object_new_int(status.item_id));
|
json_object_object_add(reply, "item_id", json_object_new_int(status.item_id));
|
||||||
json_object_object_add(reply, "item_length_ms", json_object_new_int(status.len_ms));
|
json_object_object_add(reply, "item_length_ms", json_object_new_int(status.len_ms));
|
||||||
json_object_object_add(reply, "item_progress_ms", json_object_new_int(status.pos_ms));
|
json_object_object_add(reply, "item_progress_ms", json_object_new_int(status.pos_ms));
|
||||||
json_object_object_add(reply, "artwork_url", json_object_new_string("/artwork/nowplaying"));
|
json_object_object_add(reply, "artwork_url", json_object_new_string("./artwork/nowplaying"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -2194,7 +2199,7 @@ queue_item_to_json(struct db_queue_item *queue_item, char shuffle)
|
|||||||
{
|
{
|
||||||
// Queue item does not have a valid artwork url, construct artwork url to
|
// Queue item does not have a valid artwork url, construct artwork url to
|
||||||
// get the image through the httpd_artworkapi (uses the artwork handlers).
|
// get the image through the httpd_artworkapi (uses the artwork handlers).
|
||||||
ret = snprintf(artwork_url, sizeof(artwork_url), "/artwork/item/%d", queue_item->file_id);
|
ret = snprintf(artwork_url, sizeof(artwork_url), "./artwork/item/%d", queue_item->file_id);
|
||||||
if (ret < sizeof(artwork_url))
|
if (ret < sizeof(artwork_url))
|
||||||
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
||||||
}
|
}
|
||||||
@ -2203,7 +2208,7 @@ queue_item_to_json(struct db_queue_item *queue_item, char shuffle)
|
|||||||
// Pipe and stream metadata can change if the queue version changes. Construct artwork url
|
// Pipe and stream metadata can change if the queue version changes. Construct artwork url
|
||||||
// similar to non-pipe items, but append the queue version to the url to force
|
// similar to non-pipe items, but append the queue version to the url to force
|
||||||
// clients to reload image if the queue version changes (additional metadata was found).
|
// clients to reload image if the queue version changes (additional metadata was found).
|
||||||
ret = snprintf(artwork_url, sizeof(artwork_url), "/artwork/item/%d?v=%d", queue_item->file_id, queue_item->queue_version);
|
ret = snprintf(artwork_url, sizeof(artwork_url), "./artwork/item/%d?v=%d", queue_item->file_id, queue_item->queue_version);
|
||||||
if (ret < sizeof(artwork_url))
|
if (ret < sizeof(artwork_url))
|
||||||
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
||||||
}
|
}
|
||||||
@ -3516,8 +3521,8 @@ jsonapi_reply_library_playlist_tracks(struct httpd_request *hreq)
|
|||||||
int total;
|
int total;
|
||||||
int ret = 0;
|
int ret = 0;
|
||||||
|
|
||||||
if (!is_modified(hreq->req, DB_ADMIN_DB_MODIFIED))
|
// Due to smart playlists possibly changing their tracks between rescans, disable caching in clients
|
||||||
return HTTP_NOTMODIFIED;
|
httpd_response_not_cachable(hreq->req);
|
||||||
|
|
||||||
ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &playlist_id);
|
ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &playlist_id);
|
||||||
if (ret < 0)
|
if (ret < 0)
|
||||||
|
@ -24,22 +24,29 @@
|
|||||||
#include "db.h"
|
#include "db.h"
|
||||||
#include "conffile.h"
|
#include "conffile.h"
|
||||||
|
|
||||||
// Forward - setting initializers
|
|
||||||
static bool artwork_spotify_default_getbool(struct settings_option *option);
|
|
||||||
static bool artwork_discogs_default_getbool(struct settings_option *option);
|
|
||||||
static bool artwork_coverartarchive_default_getbool(struct settings_option *option);
|
|
||||||
|
|
||||||
static struct settings_option webinterface_options[] =
|
static struct settings_option webinterface_options[] =
|
||||||
{
|
{
|
||||||
{ "show_composer_now_playing", SETTINGS_TYPE_BOOL },
|
{ "show_composer_now_playing", SETTINGS_TYPE_BOOL },
|
||||||
{ "show_composer_for_genre", SETTINGS_TYPE_STR },
|
{ "show_composer_for_genre", SETTINGS_TYPE_STR },
|
||||||
|
{ "show_cover_artwork_in_album_lists", SETTINGS_TYPE_BOOL, { true } },
|
||||||
|
{ "show_menu_item_playlists", SETTINGS_TYPE_BOOL, { true } },
|
||||||
|
{ "show_menu_item_music", SETTINGS_TYPE_BOOL, { true } },
|
||||||
|
{ "show_menu_item_podcasts", SETTINGS_TYPE_BOOL, { true } },
|
||||||
|
{ "show_menu_item_audiobooks", SETTINGS_TYPE_BOOL, { true } },
|
||||||
|
{ "show_menu_item_radio", SETTINGS_TYPE_BOOL, { false } },
|
||||||
|
{ "show_menu_item_files", SETTINGS_TYPE_BOOL, { true } },
|
||||||
|
{ "show_menu_item_search", SETTINGS_TYPE_BOOL, { true } },
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct settings_option artwork_options[] =
|
static struct settings_option artwork_options[] =
|
||||||
{
|
{
|
||||||
{ "use_artwork_source_spotify", SETTINGS_TYPE_BOOL, NULL, artwork_spotify_default_getbool, NULL },
|
// Spotify source enabled by default, it will only work for premium users anyway.
|
||||||
{ "use_artwork_source_discogs", SETTINGS_TYPE_BOOL, NULL, artwork_discogs_default_getbool, NULL },
|
// So Spotify probably won't mind, and the user probably also won't mind that we
|
||||||
{ "use_artwork_source_coverartarchive", SETTINGS_TYPE_BOOL, NULL, artwork_coverartarchive_default_getbool, NULL },
|
// share data with Spotify, since he is already doing it.
|
||||||
|
{ "use_artwork_source_spotify", SETTINGS_TYPE_BOOL, { true } },
|
||||||
|
{ "use_artwork_source_discogs", SETTINGS_TYPE_BOOL, { false } },
|
||||||
|
{ "use_artwork_source_coverartarchive", SETTINGS_TYPE_BOOL, { false } },
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct settings_option misc_options[] =
|
static struct settings_option misc_options[] =
|
||||||
@ -64,51 +71,6 @@ static struct settings_category categories[] =
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------------- DEFAULT SETTERS ------------------------------*/
|
|
||||||
|
|
||||||
static bool
|
|
||||||
artwork_default_getbool(bool no_cfg_default, const char *cfg_name)
|
|
||||||
{
|
|
||||||
cfg_t *lib = cfg_getsec(cfg, "library");
|
|
||||||
const char *name;
|
|
||||||
int n_cfg;
|
|
||||||
int i;
|
|
||||||
|
|
||||||
n_cfg = cfg_size(lib, "artwork_online_sources");
|
|
||||||
if (n_cfg == 0)
|
|
||||||
return no_cfg_default;
|
|
||||||
|
|
||||||
for (i = 0; i < n_cfg; i++)
|
|
||||||
{
|
|
||||||
name = cfg_getnstr(lib, "artwork_online_sources", i);
|
|
||||||
if (strcasecmp(name, cfg_name) == 0)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
artwork_spotify_default_getbool(struct settings_option *option)
|
|
||||||
{
|
|
||||||
// Enabled by default, it will only work for premium users anyway. So Spotify
|
|
||||||
// probably won't mind, and the user probably also won't mind that we share
|
|
||||||
// data with Spotify, since he is already doing it.
|
|
||||||
return artwork_default_getbool(true, "spotify");
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
artwork_discogs_default_getbool(struct settings_option *option)
|
|
||||||
{
|
|
||||||
return artwork_default_getbool(false, "discogs");
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
artwork_coverartarchive_default_getbool(struct settings_option *option)
|
|
||||||
{
|
|
||||||
return artwork_default_getbool(false, "coverartarchive");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------ IMPLEMENTATION -----------------------------*/
|
/* ------------------------------ IMPLEMENTATION -----------------------------*/
|
||||||
|
|
||||||
int
|
int
|
||||||
@ -185,8 +147,8 @@ settings_option_getint(struct settings_option *option)
|
|||||||
if (ret == 0)
|
if (ret == 0)
|
||||||
return intval;
|
return intval;
|
||||||
|
|
||||||
if (option->default_getint)
|
if (option->default_value.intval)
|
||||||
return option->default_getint(option);
|
return option->default_value.intval;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -204,8 +166,8 @@ settings_option_getbool(struct settings_option *option)
|
|||||||
if (ret == 0)
|
if (ret == 0)
|
||||||
return (intval != 0);
|
return (intval != 0);
|
||||||
|
|
||||||
if (option->default_getbool)
|
if (option->default_value.boolval)
|
||||||
return option->default_getbool(option);
|
return option->default_value.boolval;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -223,8 +185,8 @@ settings_option_getstr(struct settings_option *option)
|
|||||||
if (ret == 0)
|
if (ret == 0)
|
||||||
return s;
|
return s;
|
||||||
|
|
||||||
if (option->default_getstr)
|
if (option->default_value.strval)
|
||||||
return option->default_getstr(option);
|
return option->default_value.strval;
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,16 @@ enum settings_type {
|
|||||||
SETTINGS_TYPE_CATEGORY,
|
SETTINGS_TYPE_CATEGORY,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
union settings_default_value {
|
||||||
|
int intval;
|
||||||
|
bool boolval;
|
||||||
|
char *strval;
|
||||||
|
};
|
||||||
|
|
||||||
struct settings_option {
|
struct settings_option {
|
||||||
const char *name;
|
const char *name;
|
||||||
enum settings_type type;
|
enum settings_type type;
|
||||||
int (*default_getint)(struct settings_option *option);
|
union settings_default_value default_value;
|
||||||
bool (*default_getbool)(struct settings_option *option);
|
|
||||||
char *(*default_getstr)(struct settings_option *option);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct settings_category {
|
struct settings_category {
|
||||||
|
3752
web-src/package-lock.json
generated
3752
web-src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "forked-daapd-web",
|
"name": "forked-daapd-web",
|
||||||
"version": "0.7.2",
|
"version": "0.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "forked-daapd web interface",
|
"description": "forked-daapd web interface",
|
||||||
"author": "chme <christian.meffert@googlemail.com>",
|
"author": "chme <christian.meffert@googlemail.com>",
|
||||||
@ -11,41 +11,44 @@
|
|||||||
"dev": "vue-cli-service serve"
|
"dev": "vue-cli-service serve"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.20.0",
|
||||||
"bulma": "^0.9.0",
|
"bulma": "^0.9.0",
|
||||||
|
"bulma-switch": "^2.0.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"mdi": "^2.2.43",
|
"mdi": "^2.2.43",
|
||||||
"moment": "^2.27.0",
|
"moment": "^2.28.0",
|
||||||
"moment-duration-format": "^2.3.2",
|
"moment-duration-format": "^2.3.2",
|
||||||
"npm": "^6.14.5",
|
"npm": "^6.14.8",
|
||||||
"reconnectingwebsocket": "^1.0.0",
|
"reconnectingwebsocket": "^1.0.0",
|
||||||
"spotify-web-api-js": "^1.4.0",
|
"spotify-web-api-js": "^1.5.0",
|
||||||
"string-to-color": "^2.1.4",
|
"string-to-color": "^2.2.2",
|
||||||
"v-click-outside": "^3.0.1",
|
"v-click-outside": "^3.1.2",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.12",
|
||||||
"vue-infinite-loading": "^2.4.5",
|
"vue-infinite-loading": "^2.4.5",
|
||||||
|
"vue-observe-visibility": "^0.4.6",
|
||||||
"vue-progressbar": "^0.7.5",
|
"vue-progressbar": "^0.7.5",
|
||||||
"vue-range-slider": "^0.6.0",
|
"vue-range-slider": "^0.6.0",
|
||||||
"vue-router": "^3.3.4",
|
"vue-router": "^3.4.3",
|
||||||
|
"vue-scrollto": "^2.18.2",
|
||||||
"vue-tiny-lazyload-img": "^0.1.0",
|
"vue-tiny-lazyload-img": "^0.1.0",
|
||||||
"vuedraggable": "^2.23.2",
|
"vuedraggable": "^2.24.1",
|
||||||
"vuex": "^3.5.1"
|
"vuex": "^3.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.4.6",
|
"@vue/cli-plugin-babel": "^4.5.6",
|
||||||
"@vue/cli-plugin-eslint": "^4.4.6",
|
"@vue/cli-plugin-eslint": "^4.5.6",
|
||||||
"@vue/cli-service": "^4.4.6",
|
"@vue/cli-service": "^4.5.6",
|
||||||
"@vue/eslint-config-standard": "^5.1.2",
|
"@vue/eslint-config-standard": "^5.1.2",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"eslint": "^7.3.1",
|
"eslint": "^7.9.0",
|
||||||
"eslint-plugin-import": "^2.22.0",
|
"eslint-plugin-import": "^2.22.0",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-standard": "^4.0.1",
|
"eslint-plugin-standard": "^4.0.1",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
"sass": "^1.26.9",
|
"sass": "^1.26.11",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^10.0.2",
|
||||||
"vue-template-compiler": "^2.6.11"
|
"vue-template-compiler": "^2.6.12"
|
||||||
},
|
},
|
||||||
"license": "GPL-2.0"
|
"license": "GPL-2.0"
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<title>forked-daapd-web 2</title>
|
<title>forked-daapd-web 2</title>
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon.png?ver1.1">
|
<link rel="apple-touch-icon" sizes="120x120" href="apple-touch-icon.png?ver1.1">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
<link rel="manifest" href="site.webmanifest">
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5">
|
||||||
<meta name="msapplication-TileColor" content="#da532c">
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
</head>
|
</head>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<figure>
|
<figure>
|
||||||
<img v-lazyload
|
<img v-lazyload
|
||||||
:src="dataURI"
|
|
||||||
:data-src="artwork_url_with_size"
|
:data-src="artwork_url_with_size"
|
||||||
:data-err="dataURI"
|
:data-err="dataURI"
|
||||||
@click="$emit('click')">
|
@click="$emit('click')">
|
||||||
@ -15,7 +14,7 @@ import stringToColor from 'string-to-color'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CoverArtwork',
|
name: 'CoverArtwork',
|
||||||
props: ['artist', 'album', 'artwork_url'],
|
props: ['artist', 'album', 'artwork_url', 'maxwidth', 'maxheight'],
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -30,6 +29,9 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
artwork_url_with_size: function () {
|
artwork_url_with_size: function () {
|
||||||
|
if (this.maxwidth > 0 && this.maxheight > 0) {
|
||||||
|
return webapi.artwork_url_append_size_params(this.artwork_url, this.maxwidth, this.maxheight)
|
||||||
|
}
|
||||||
return webapi.artwork_url_append_size_params(this.artwork_url)
|
return webapi.artwork_url_append_size_params(this.artwork_url)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
50
web-src/src/components/DropdownMenu.vue
Normal file
50
web-src/src/components/DropdownMenu.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dropdown" :class="{ 'is-active': is_active }" v-click-outside="onClickOutside">
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu" @click="is_active = !is_active">
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="mdi mdi-chevron-down" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a class="dropdown-item"
|
||||||
|
v-for="option in options" :key="option"
|
||||||
|
:class="{'is-active': value === option}"
|
||||||
|
@click="select(option)">
|
||||||
|
{{ option }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DropdownMenu',
|
||||||
|
|
||||||
|
props: ['value', 'options'],
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
is_active: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onClickOutside (event) {
|
||||||
|
this.is_active = false
|
||||||
|
},
|
||||||
|
|
||||||
|
select (option) {
|
||||||
|
this.is_active = false
|
||||||
|
this.$emit('input', option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
@ -1,11 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<section>
|
<section>
|
||||||
<nav class="buttons is-centered fd-is-square" style="margin-bottom: 48px;" v-if="filtered_index.length > 1">
|
<nav class="buttons is-centered fd-is-square" style="margin-bottom: 16px;">
|
||||||
<a v-for="char in filtered_index" :key="char" class="button is-small" @click="nav(char)">{{ char }}</a>
|
<a v-for="char in filtered_index" :key="char" class="button is-small" @click="nav(char)">{{ char }}</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="buttons is-centered" style="margin-bottom: 6px;" v-if="filtered_index.length > 1">
|
|
||||||
<a class="button is-small is-white" @click="scroll_to_top"><span class="icon is-small"><i class="mdi mdi-chevron-up"></i></span></a>
|
|
||||||
</nav>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
159
web-src/src/components/ListAlbums.vue
Normal file
159
web-src/src/components/ListAlbums.vue
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="is_grouped">
|
||||||
|
<div v-for="idx in albums.indexList" :key="idx" class="mb-6">
|
||||||
|
<span class="tag is-info is-light is-small has-text-weight-bold" :id="'index_' + idx">{{ idx }}</span>
|
||||||
|
<list-item-album v-for="album in albums.grouped[idx]"
|
||||||
|
:key="album.id"
|
||||||
|
:album="album"
|
||||||
|
@click="open_album(album)">
|
||||||
|
<template slot="artwork" v-if="is_visible_artwork">
|
||||||
|
<p class="image is-64x64 fd-has-shadow fd-has-action">
|
||||||
|
<cover-artwork
|
||||||
|
:artwork_url="album.artwork_url"
|
||||||
|
:artist="album.artist"
|
||||||
|
:album="album.name"
|
||||||
|
:maxwidth="64"
|
||||||
|
:maxheight="64" />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template slot="actions">
|
||||||
|
<a @click="open_dialog(album)">
|
||||||
|
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</list-item-album>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<list-item-album v-for="album in albums_list"
|
||||||
|
:key="album.id"
|
||||||
|
:album="album"
|
||||||
|
@click="open_album(album)">
|
||||||
|
<template slot="artwork" v-if="is_visible_artwork">
|
||||||
|
<p class="image is-64x64 fd-has-shadow fd-has-action">
|
||||||
|
<cover-artwork
|
||||||
|
:artwork_url="album.artwork_url"
|
||||||
|
:artist="album.artist"
|
||||||
|
:album="album.name"
|
||||||
|
:maxwidth="64"
|
||||||
|
:maxheight="64" />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template slot="actions">
|
||||||
|
<a @click="open_dialog(album)">
|
||||||
|
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</list-item-album>
|
||||||
|
</div>
|
||||||
|
<modal-dialog-album
|
||||||
|
:show="show_details_modal"
|
||||||
|
:album="selected_album"
|
||||||
|
:media_kind="media_kind"
|
||||||
|
@remove-podcast="open_remove_podcast_dialog()"
|
||||||
|
@close="show_details_modal = false" />
|
||||||
|
<modal-dialog
|
||||||
|
:show="show_remove_podcast_modal"
|
||||||
|
title="Remove podcast"
|
||||||
|
delete_action="Remove"
|
||||||
|
@close="show_remove_podcast_modal = false"
|
||||||
|
@delete="remove_podcast">
|
||||||
|
<template slot="modal-content">
|
||||||
|
<p>Permanently remove this podcast from your library?</p>
|
||||||
|
<p class="is-size-7">(This will also remove the RSS playlist <b>{{ rss_playlist_to_remove.name }}</b>.)</p>
|
||||||
|
</template>
|
||||||
|
</modal-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||||
|
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||||
|
import ModalDialog from '@/components/ModalDialog'
|
||||||
|
import CoverArtwork from '@/components/CoverArtwork'
|
||||||
|
import webapi from '@/webapi'
|
||||||
|
import Albums from '@/lib/Albums'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ListAlbums',
|
||||||
|
components: { ListItemAlbum, ModalDialogAlbum, ModalDialog, CoverArtwork },
|
||||||
|
|
||||||
|
props: ['albums', 'media_kind'],
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
show_details_modal: false,
|
||||||
|
selected_album: {},
|
||||||
|
|
||||||
|
show_remove_podcast_modal: false,
|
||||||
|
rss_playlist_to_remove: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
is_visible_artwork () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_cover_artwork_in_album_lists').value
|
||||||
|
},
|
||||||
|
|
||||||
|
media_kind_resolved: function () {
|
||||||
|
return this.media_kind ? this.media_kind : this.selected_album.media_kind
|
||||||
|
},
|
||||||
|
|
||||||
|
albums_list: function () {
|
||||||
|
if (Array.isArray(this.albums)) {
|
||||||
|
return this.albums
|
||||||
|
}
|
||||||
|
return this.albums.sortedAndFiltered
|
||||||
|
},
|
||||||
|
|
||||||
|
is_grouped: function () {
|
||||||
|
return (this.albums instanceof Albums && this.albums.options.group)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
open_album: function (album) {
|
||||||
|
this.selected_album = album
|
||||||
|
if (this.media_kind_resolved === 'podcast') {
|
||||||
|
this.$router.push({ path: '/podcasts/' + album.id })
|
||||||
|
} else if (this.media_kind_resolved === 'audiobook') {
|
||||||
|
this.$router.push({ path: '/audiobooks/' + album.id })
|
||||||
|
} else {
|
||||||
|
this.$router.push({ path: '/music/albums/' + album.id })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
open_dialog: function (album) {
|
||||||
|
this.selected_album = album
|
||||||
|
this.show_details_modal = true
|
||||||
|
},
|
||||||
|
|
||||||
|
open_remove_podcast_dialog: function () {
|
||||||
|
webapi.library_album_tracks(this.selected_album.id, { limit: 1 }).then(({ data }) => {
|
||||||
|
webapi.library_track_playlists(data.items[0].id).then(({ data }) => {
|
||||||
|
const rssPlaylists = data.items.filter(pl => pl.type === 'rss')
|
||||||
|
if (rssPlaylists.length !== 1) {
|
||||||
|
this.$store.dispatch('add_notification', { text: 'Podcast cannot be removed. Probably it was not added as an RSS playlist.', type: 'danger' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rss_playlist_to_remove = rssPlaylists[0]
|
||||||
|
this.show_remove_podcast_modal = true
|
||||||
|
this.show_details_modal = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
remove_podcast: function () {
|
||||||
|
this.show_remove_podcast_modal = false
|
||||||
|
webapi.library_playlist_delete(this.rss_playlist_to_remove.id).then(() => {
|
||||||
|
this.$emit('podcast-deleted')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
90
web-src/src/components/ListArtists.vue
Normal file
90
web-src/src/components/ListArtists.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="is_grouped">
|
||||||
|
<div v-for="idx in artists.indexList" :key="idx" class="mb-6">
|
||||||
|
<span class="tag is-info is-light is-small has-text-weight-bold" :id="'index_' + idx">{{ idx }}</span>
|
||||||
|
<list-item-artist v-for="artist in artists.grouped[idx]"
|
||||||
|
:key="artist.id"
|
||||||
|
:artist="artist"
|
||||||
|
@click="open_artist(artist)">
|
||||||
|
<template slot="actions">
|
||||||
|
<a @click="open_dialog(artist)">
|
||||||
|
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</list-item-artist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<list-item-artist v-for="artist in artists_list"
|
||||||
|
:key="artist.id"
|
||||||
|
:artist="artist"
|
||||||
|
@click="open_artist(artist)">
|
||||||
|
<template slot="actions">
|
||||||
|
<a @click="open_dialog(artist)">
|
||||||
|
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</list-item-artist>
|
||||||
|
</div>
|
||||||
|
<modal-dialog-artist :show="show_details_modal" :artist="selected_artist" :media_kind="media_kind" @close="show_details_modal = false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ListItemArtist from '@/components/ListItemArtist'
|
||||||
|
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||||
|
import Artists from '@/lib/Artists'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ListArtists',
|
||||||
|
components: { ListItemArtist, ModalDialogArtist },
|
||||||
|
|
||||||
|
props: ['artists', 'media_kind'],
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
show_details_modal: false,
|
||||||
|
selected_artist: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
media_kind_resolved: function () {
|
||||||
|
return this.media_kind ? this.media_kind : this.selected_artist.media_kind
|
||||||
|
},
|
||||||
|
|
||||||
|
artists_list: function () {
|
||||||
|
if (Array.isArray(this.artists)) {
|
||||||
|
return this.artists
|
||||||
|
}
|
||||||
|
return this.artists.sortedAndFiltered
|
||||||
|
},
|
||||||
|
|
||||||
|
is_grouped: function () {
|
||||||
|
return (this.artists instanceof Artists && this.artists.options.group)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
open_artist: function (artist) {
|
||||||
|
this.selected_artist = artist
|
||||||
|
if (this.media_kind_resolved === 'podcast') {
|
||||||
|
// No artist page for podcasts
|
||||||
|
} else if (this.media_kind_resolved === 'audiobook') {
|
||||||
|
this.$router.push({ path: '/audiobooks/artists/' + artist.id })
|
||||||
|
} else {
|
||||||
|
this.$router.push({ path: '/music/artists/' + artist.id })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
open_dialog: function (artist) {
|
||||||
|
this.selected_artist = artist
|
||||||
|
this.show_details_modal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
@ -1,6 +1,10 @@
|
|||||||
<template functional>
|
<template functional>
|
||||||
<div class="media" :id="'index_' + props.album.name_sort.charAt(0).toUpperCase()">
|
<div class="media" :id="'index_' + props.album.name_sort.charAt(0).toUpperCase()">
|
||||||
<slot name="artwork"></slot>
|
<div class="media-left fd-has-action"
|
||||||
|
v-if="$slots['artwork']"
|
||||||
|
@click="listeners.click">
|
||||||
|
<slot name="artwork"></slot>
|
||||||
|
</div>
|
||||||
<div class="media-content fd-has-action is-clipped" @click="listeners.click">
|
<div class="media-content fd-has-action is-clipped" @click="listeners.click">
|
||||||
<div style="margin-top:0.7rem;">
|
<div style="margin-top:0.7rem;">
|
||||||
<h1 class="title is-6">{{ props.album.name }}</h1>
|
<h1 class="title is-6">{{ props.album.name }}</h1>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template functional>
|
<template functional>
|
||||||
<div class="media" :id="'index_' + props.artist.name_sort.charAt(0).toUpperCase()">
|
<div class="media">
|
||||||
<div class="media-content fd-has-action is-clipped" @click="listeners.click">
|
<div class="media-content fd-has-action is-clipped" @click="listeners.click">
|
||||||
<h1 class="title is-6">{{ props.artist.name }}</h1>
|
<h1 class="title is-6">{{ props.artist.name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
54
web-src/src/components/ListPlaylists.vue
Normal file
54
web-src/src/components/ListPlaylists.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<list-item-playlist v-for="playlist in playlists" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
|
||||||
|
<template slot="icon">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="mdi" :class="{ 'mdi-library-music': playlist.type !== 'folder', 'mdi-rss': playlist.type === 'rss', 'mdi-folder': playlist.type === 'folder' }"></i>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template slot="actions">
|
||||||
|
<a @click="open_dialog(playlist)">
|
||||||
|
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</list-item-playlist>
|
||||||
|
<modal-dialog-playlist :show="show_details_modal" :playlist="selected_playlist" @close="show_details_modal = false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ListItemPlaylist from '@/components/ListItemPlaylist'
|
||||||
|
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ListPlaylists',
|
||||||
|
components: { ListItemPlaylist, ModalDialogPlaylist },
|
||||||
|
|
||||||
|
props: ['playlists'],
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
show_details_modal: false,
|
||||||
|
selected_playlist: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
open_playlist: function (playlist) {
|
||||||
|
if (playlist.type !== 'folder') {
|
||||||
|
this.$router.push({ path: '/playlists/' + playlist.id + '/tracks' })
|
||||||
|
} else {
|
||||||
|
this.$router.push({ path: '/playlists/' + playlist.id })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
open_dialog: function (playlist) {
|
||||||
|
this.selected_playlist = playlist
|
||||||
|
this.show_details_modal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
52
web-src/src/components/ListTracks.vue
Normal file
52
web-src/src/components/ListTracks.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" @click="play_track(index, track)">
|
||||||
|
<template slot="actions">
|
||||||
|
<a @click="open_dialog(track)">
|
||||||
|
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</list-item-track>
|
||||||
|
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ListItemTrack from '@/components/ListItemTrack'
|
||||||
|
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||||
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ListTracks',
|
||||||
|
components: { ListItemTrack, ModalDialogTrack },
|
||||||
|
|
||||||
|
props: ['tracks', 'uris', 'expression'],
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
show_details_modal: false,
|
||||||
|
selected_track: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
play_track: function (position, track) {
|
||||||
|
if (this.uris) {
|
||||||
|
webapi.player_play_uri(this.uris, false, position)
|
||||||
|
} else if (this.expression) {
|
||||||
|
webapi.player_play_expression(this.expression, false, position)
|
||||||
|
} else {
|
||||||
|
webapi.player_play_uri(track.uri, false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
open_dialog: function (track) {
|
||||||
|
this.selected_track = track
|
||||||
|
this.show_details_modal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
@ -14,23 +14,39 @@
|
|||||||
<p class="title is-4">
|
<p class="title is-4">
|
||||||
<a class="has-text-link" @click="open_album">{{ album.name }}</a>
|
<a class="has-text-link" @click="open_album">{{ album.name }}</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="buttons" v-if="media_kind === 'podcast'">
|
<div class="buttons" v-if="media_kind_resolved === 'podcast'">
|
||||||
<a class="button is-small" @click="mark_played">Mark as played</a>
|
<a class="button is-small" @click="mark_played">Mark as played</a>
|
||||||
<a class="button is-small" @click="$emit('remove_podcast')">Remove podcast</a>
|
<a class="button is-small" @click="$emit('remove-podcast')">Remove podcast</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="content is-small">
|
<div class="content is-small">
|
||||||
<p v-if="album.artist && media_kind !== 'audiobook'">
|
<p v-if="album.artist">
|
||||||
<span class="heading">Album artist</span>
|
<span class="heading">Album artist</span>
|
||||||
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artist }}</a>
|
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artist }}</a>
|
||||||
</p>
|
</p>
|
||||||
<p v-if="album.artist && media_kind === 'audiobook'">
|
<p v-if="album.date_released">
|
||||||
<span class="heading">Album artist</span>
|
<span class="heading">Release date</span>
|
||||||
<span class="title is-6">{{ album.artist }}</span>
|
<span class="title is-6">{{ album.date_released | time('L') }}</span>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="album.year > 0">
|
||||||
|
<span class="heading">Year</span>
|
||||||
|
<span class="title is-6">{{ album.year }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span class="heading">Tracks</span>
|
<span class="heading">Tracks</span>
|
||||||
<span class="title is-6">{{ album.track_count }}</span>
|
<span class="title is-6">{{ album.track_count }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="heading">Length</span>
|
||||||
|
<span class="title is-6">{{ album.length_ms | duration }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="heading">Type</span>
|
||||||
|
<span class="title is-6">{{ album.media_kind }} - {{ album.data_kind }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="heading">Added at</span>
|
||||||
|
<span class="title is-6">{{ album.time_added | time('L LT') }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="card-footer">
|
<footer class="card-footer">
|
||||||
@ -70,6 +86,10 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
artwork_url: function () {
|
artwork_url: function () {
|
||||||
return webapi.artwork_url_append_size_params(this.album.artwork_url)
|
return webapi.artwork_url_append_size_params(this.album.artwork_url)
|
||||||
|
},
|
||||||
|
|
||||||
|
media_kind_resolved: function () {
|
||||||
|
return this.media_kind ? this.media_kind : this.album.media_kind
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -90,9 +110,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
open_album: function () {
|
open_album: function () {
|
||||||
if (this.media_kind === 'podcast') {
|
if (this.media_kind_resolved === 'podcast') {
|
||||||
this.$router.push({ path: '/podcasts/' + this.album.id })
|
this.$router.push({ path: '/podcasts/' + this.album.id })
|
||||||
} else if (this.media_kind === 'audiobook') {
|
} else if (this.media_kind_resolved === 'audiobook') {
|
||||||
this.$router.push({ path: '/audiobooks/' + this.album.id })
|
this.$router.push({ path: '/audiobooks/' + this.album.id })
|
||||||
} else {
|
} else {
|
||||||
this.$router.push({ path: '/music/albums/' + this.album.id })
|
this.$router.push({ path: '/music/albums/' + this.album.id })
|
||||||
@ -100,7 +120,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
open_artist: function () {
|
open_artist: function () {
|
||||||
this.$router.push({ path: '/music/artists/' + this.album.artist_id })
|
if (this.media_kind_resolved === 'podcast') {
|
||||||
|
// No artist page for podcasts
|
||||||
|
} else if (this.media_kind_resolved === 'audiobook') {
|
||||||
|
this.$router.push({ path: '/audiobooks/artists/' + this.album.artist_id })
|
||||||
|
} else {
|
||||||
|
this.$router.push({ path: '/music/artists/' + this.album.artist_id })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mark_played: function () {
|
mark_played: function () {
|
||||||
|
@ -18,6 +18,14 @@
|
|||||||
<span class="heading">Tracks</span>
|
<span class="heading">Tracks</span>
|
||||||
<span class="title is-6">{{ artist.track_count }}</span>
|
<span class="title is-6">{{ artist.track_count }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="heading">Type</span>
|
||||||
|
<span class="title is-6">{{ artist.data_kind }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="heading">Added at</span>
|
||||||
|
<span class="title is-6">{{ artist.time_added | time('L LT') }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="card-footer">
|
<footer class="card-footer">
|
||||||
|
@ -44,7 +44,7 @@ import webapi from '@/webapi'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ModalDialogPlaylist',
|
name: 'ModalDialogPlaylist',
|
||||||
props: ['show', 'playlist'],
|
props: ['show', 'playlist', 'tracks'],
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
play: function () {
|
play: function () {
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="level-item fd-expanded">
|
<div class="level-item fd-expanded">
|
||||||
<div class="fd-expanded">
|
<div class="fd-expanded">
|
||||||
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="/stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p>
|
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p>
|
||||||
<range-slider
|
<range-slider
|
||||||
class="slider fd-has-action"
|
class="slider fd-has-action"
|
||||||
min="0"
|
min="0"
|
||||||
@ -174,7 +174,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="level-item fd-expanded">
|
<div class="level-item fd-expanded">
|
||||||
<div class="fd-expanded">
|
<div class="fd-expanded">
|
||||||
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="/stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p>
|
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p>
|
||||||
<range-slider
|
<range-slider
|
||||||
class="slider fd-has-action"
|
class="slider fd-has-action"
|
||||||
min="0"
|
min="0"
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="fd-top-navbar navbar is-light is-fixed-top" :style="zindex" role="navigation" aria-label="main navigation">
|
<nav class="fd-top-navbar navbar is-light is-fixed-top" :style="zindex" role="navigation" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<navbar-item-link to="/playlists">
|
<navbar-item-link to="/playlists" v-if="is_visible_playlists">
|
||||||
<span class="icon"><i class="mdi mdi-library-music"></i></span>
|
<span class="icon"><i class="mdi mdi-library-music"></i></span>
|
||||||
</navbar-item-link>
|
</navbar-item-link>
|
||||||
<navbar-item-link to="/music">
|
<navbar-item-link to="/music" v-if="is_visible_music">
|
||||||
<span class="icon"><i class="mdi mdi-music"></i></span>
|
<span class="icon"><i class="mdi mdi-music"></i></span>
|
||||||
</navbar-item-link>
|
</navbar-item-link>
|
||||||
<navbar-item-link to="/podcasts">
|
<navbar-item-link to="/podcasts" v-if="is_visible_podcasts">
|
||||||
<span class="icon"><i class="mdi mdi-microphone"></i></span>
|
<span class="icon"><i class="mdi mdi-microphone"></i></span>
|
||||||
</navbar-item-link>
|
</navbar-item-link>
|
||||||
<navbar-item-link to="/audiobooks" v-if="audiobooks.tracks > 0">
|
<navbar-item-link to="/audiobooks" v-if="is_visible_audiobooks">
|
||||||
<span class="icon"><i class="mdi mdi-book-open-variant"></i></span>
|
<span class="icon"><i class="mdi mdi-book-open-variant"></i></span>
|
||||||
</navbar-item-link>
|
</navbar-item-link>
|
||||||
<navbar-item-link to="/files">
|
<navbar-item-link to="/radio" v-if="is_visible_radio">
|
||||||
|
<span class="icon"><i class="mdi mdi-radio"></i></span>
|
||||||
|
</navbar-item-link>
|
||||||
|
<navbar-item-link to="/files" v-if="is_visible_files">
|
||||||
<span class="icon"><i class="mdi mdi-folder-open"></i></span>
|
<span class="icon"><i class="mdi mdi-folder-open"></i></span>
|
||||||
</navbar-item-link>
|
</navbar-item-link>
|
||||||
<navbar-item-link to="/search">
|
<navbar-item-link to="/search" v-if="is_visible_search">
|
||||||
<span class="icon"><i class="mdi mdi-magnify"></i></span>
|
<span class="icon"><i class="mdi mdi-magnify"></i></span>
|
||||||
</navbar-item-link>
|
</navbar-item-link>
|
||||||
|
|
||||||
@ -49,16 +52,16 @@
|
|||||||
<navbar-item-link to="/music/artists"><span class="fd-navbar-item-level2">Artists</span></navbar-item-link>
|
<navbar-item-link to="/music/artists"><span class="fd-navbar-item-level2">Artists</span></navbar-item-link>
|
||||||
<navbar-item-link to="/music/albums"><span class="fd-navbar-item-level2">Albums</span></navbar-item-link>
|
<navbar-item-link to="/music/albums"><span class="fd-navbar-item-level2">Albums</span></navbar-item-link>
|
||||||
<navbar-item-link to="/music/genres"><span class="fd-navbar-item-level2">Genres</span></navbar-item-link>
|
<navbar-item-link to="/music/genres"><span class="fd-navbar-item-level2">Genres</span></navbar-item-link>
|
||||||
<navbar-item-link to="/music/radio"><span class="fd-navbar-item-level2">Radio</span></navbar-item-link>
|
|
||||||
<navbar-item-link to="/music/spotify" v-if="spotify_enabled"><span class="fd-navbar-item-level2">Spotify</span></navbar-item-link>
|
<navbar-item-link to="/music/spotify" v-if="spotify_enabled"><span class="fd-navbar-item-level2">Spotify</span></navbar-item-link>
|
||||||
<navbar-item-link to="/podcasts"><span class="icon"><i class="mdi mdi-microphone"></i></span> <b>Podcasts</b></navbar-item-link>
|
<navbar-item-link to="/podcasts"><span class="icon"><i class="mdi mdi-microphone"></i></span> <b>Podcasts</b></navbar-item-link>
|
||||||
<navbar-item-link to="/audiobooks"><span class="icon"><i class="mdi mdi-book-open-variant"></i></span> <b>Audiobooks</b></navbar-item-link>
|
<navbar-item-link to="/audiobooks"><span class="icon"><i class="mdi mdi-book-open-variant"></i></span> <b>Audiobooks</b></navbar-item-link>
|
||||||
|
<navbar-item-link to="/radio"><span class="icon"><i class="mdi mdi-radio"></i></span> <b>Radio</b></navbar-item-link>
|
||||||
<navbar-item-link to="/files"><span class="icon"><i class="mdi mdi-folder-open"></i></span> <b>Files</b></navbar-item-link>
|
<navbar-item-link to="/files"><span class="icon"><i class="mdi mdi-folder-open"></i></span> <b>Files</b></navbar-item-link>
|
||||||
<navbar-item-link to="/search"><span class="icon"><i class="mdi mdi-magnify"></i></span> <b>Search</b></navbar-item-link>
|
<navbar-item-link to="/search"><span class="icon"><i class="mdi mdi-magnify"></i></span> <b>Search</b></navbar-item-link>
|
||||||
<hr class="fd-navbar-divider">
|
<hr class="fd-navbar-divider">
|
||||||
|
|
||||||
<navbar-item-link to="/settings/webinterface">Settings</navbar-item-link>
|
<navbar-item-link to="/settings/webinterface">Settings</navbar-item-link>
|
||||||
<navbar-item-link to="/about">About</navbar-item-link>
|
<navbar-item-link to="/about">Update Library</navbar-item-link>
|
||||||
|
|
||||||
<div class="navbar-item is-hidden-desktop" style="margin-bottom: 2.5rem;"></div>
|
<div class="navbar-item is-hidden-desktop" style="margin-bottom: 2.5rem;"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -87,6 +90,28 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
is_visible_playlists () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_menu_item_playlists').value
|
||||||
|
},
|
||||||
|
is_visible_music () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_menu_item_music').value
|
||||||
|
},
|
||||||
|
is_visible_podcasts () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_menu_item_podcasts').value
|
||||||
|
},
|
||||||
|
is_visible_audiobooks () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_menu_item_audiobooks').value
|
||||||
|
},
|
||||||
|
is_visible_radio () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_menu_item_radio').value
|
||||||
|
},
|
||||||
|
is_visible_files () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_menu_item_files').value
|
||||||
|
},
|
||||||
|
is_visible_search () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_menu_item_search').value
|
||||||
|
},
|
||||||
|
|
||||||
player () {
|
player () {
|
||||||
return this.$store.state.player
|
return this.$store.state.player
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
<template>
|
<template functional>
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-content fd-has-action is-clipped" v-on:click="open_album">
|
<div class="media-left fd-has-action"
|
||||||
<h1 class="title is-6">{{ album.name }}</h1>
|
v-if="$slots['artwork']"
|
||||||
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artists[0].name }}</b></h2>
|
@click="listeners.click">
|
||||||
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal">({{ album.album_type }}, {{ album.release_date | time('L') }})</h2>
|
<slot name="artwork"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="media-content fd-has-action is-clipped" @click="listeners.click">
|
||||||
|
<h1 class="title is-6">{{ props.album.name }}</h1>
|
||||||
|
<h2 class="subtitle is-7 has-text-grey"><b>{{ props.album.artists[0].name }}</b></h2>
|
||||||
|
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal">({{ props.album.album_type }}, {{ props.album.release_date | time('L') }})</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-right">
|
<div class="media-right">
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
@ -14,14 +19,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'SpotifyListItemAlbum',
|
name: 'SpotifyListItemAlbum',
|
||||||
|
props: ['album']
|
||||||
props: ['album'],
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
open_album: function () {
|
|
||||||
this.$router.push({ path: '/music/spotify/albums/' + this.album.id })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
35
web-src/src/components/TabsAudiobooks.vue
Normal file
35
web-src/src/components/TabsAudiobooks.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<section class="section fd-tabs-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns is-centered">
|
||||||
|
<div class="column is-four-fifths">
|
||||||
|
<div class="tabs is-centered is-small">
|
||||||
|
<ul>
|
||||||
|
<router-link tag="li" to="/audiobooks/artists" active-class="is-active">
|
||||||
|
<a>
|
||||||
|
<span class="icon is-small"><i class="mdi mdi-artist"></i></span>
|
||||||
|
<span class="">Authors</span>
|
||||||
|
</a>
|
||||||
|
</router-link>
|
||||||
|
<router-link tag="li" to="/audiobooks/albums" active-class="is-active">
|
||||||
|
<a>
|
||||||
|
<span class="icon is-small"><i class="mdi mdi-album"></i></span>
|
||||||
|
<span class="">Audiobooks</span>
|
||||||
|
</a>
|
||||||
|
</router-link>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TabsAudiobooks'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
@ -29,12 +29,6 @@
|
|||||||
<span class="">Genres</span>
|
<span class="">Genres</span>
|
||||||
</a>
|
</a>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link tag="li" to="/music/radio" active-class="is-active">
|
|
||||||
<a>
|
|
||||||
<span class="icon is-small"><i class="mdi mdi-radio"></i></span>
|
|
||||||
<span class="">Radio</span>
|
|
||||||
</a>
|
|
||||||
</router-link>
|
|
||||||
<router-link tag="li" to="/music/spotify" v-if="spotify_enabled" active-class="is-active">
|
<router-link tag="li" to="/music/spotify" v-if="spotify_enabled" active-class="is-active">
|
||||||
<a>
|
<a>
|
||||||
<span class="icon is-small"><i class="mdi mdi-spotify"></i></span>
|
<span class="icon is-small"><i class="mdi mdi-spotify"></i></span>
|
||||||
|
74
web-src/src/lib/Albums.js
Normal file
74
web-src/src/lib/Albums.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
export default class Albums {
|
||||||
|
constructor (items, options = { hideSingles: false, hideSpotify: false, sort: 'Name', group: false }) {
|
||||||
|
this.items = items
|
||||||
|
this.options = options
|
||||||
|
this.grouped = {}
|
||||||
|
this.sortedAndFiltered = []
|
||||||
|
this.indexList = []
|
||||||
|
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init () {
|
||||||
|
this.createSortedAndFilteredList()
|
||||||
|
this.createGroupedList()
|
||||||
|
this.createIndexList()
|
||||||
|
}
|
||||||
|
|
||||||
|
getAlbumIndex (album) {
|
||||||
|
if (this.options.sort === 'Recently added') {
|
||||||
|
return album.time_added.substring(0, 4)
|
||||||
|
} else if (this.options.sort === 'Recently released') {
|
||||||
|
return album.date_released ? album.date_released.substring(0, 4) : '0000'
|
||||||
|
}
|
||||||
|
return album.name_sort.charAt(0).toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
isAlbumVisible (album) {
|
||||||
|
if (this.options.hideSingles && album.track_count <= 2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.options.hideSpotify && album.data_kind === 'spotify') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
createIndexList () {
|
||||||
|
this.indexList = [...new Set(this.sortedAndFiltered
|
||||||
|
.map(album => this.getAlbumIndex(album)))]
|
||||||
|
}
|
||||||
|
|
||||||
|
createSortedAndFilteredList () {
|
||||||
|
var albumsSorted = this.items
|
||||||
|
if (this.options.hideSingles || this.options.hideSpotify || this.options.hideOther) {
|
||||||
|
albumsSorted = albumsSorted.filter(album => this.isAlbumVisible(album))
|
||||||
|
}
|
||||||
|
if (this.options.sort === 'Recently added') {
|
||||||
|
albumsSorted = [...albumsSorted].sort((a, b) => b.time_added.localeCompare(a.time_added))
|
||||||
|
} else if (this.options.sort === 'Recently released') {
|
||||||
|
albumsSorted = [...albumsSorted].sort((a, b) => {
|
||||||
|
if (!a.date_released) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if (!b.date_released) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return b.date_released.localeCompare(a.date_released)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.sortedAndFiltered = albumsSorted
|
||||||
|
}
|
||||||
|
|
||||||
|
createGroupedList () {
|
||||||
|
if (!this.options.group) {
|
||||||
|
this.grouped = {}
|
||||||
|
}
|
||||||
|
this.grouped = this.sortedAndFiltered.reduce((r, album) => {
|
||||||
|
const idx = this.getAlbumIndex(album)
|
||||||
|
r[idx] = [...r[idx] || [], album]
|
||||||
|
return r
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
}
|
62
web-src/src/lib/Artists.js
Normal file
62
web-src/src/lib/Artists.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
export default class Artists {
|
||||||
|
constructor (items, options = { hideSingles: false, hideSpotify: false, sort: 'Name', group: false }) {
|
||||||
|
this.items = items
|
||||||
|
this.options = options
|
||||||
|
this.grouped = {}
|
||||||
|
this.sortedAndFiltered = []
|
||||||
|
this.indexList = []
|
||||||
|
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init () {
|
||||||
|
this.createSortedAndFilteredList()
|
||||||
|
this.createGroupedList()
|
||||||
|
this.createIndexList()
|
||||||
|
}
|
||||||
|
|
||||||
|
getArtistIndex (artist) {
|
||||||
|
if (this.options.sort === 'Name') {
|
||||||
|
return artist.name_sort.charAt(0).toUpperCase()
|
||||||
|
}
|
||||||
|
return artist.time_added.substring(0, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
isArtistVisible (artist) {
|
||||||
|
if (this.options.hideSingles && artist.track_count <= (artist.album_count * 2)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.options.hideSpotify && artist.data_kind === 'spotify') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
createIndexList () {
|
||||||
|
this.indexList = [...new Set(this.sortedAndFiltered
|
||||||
|
.map(artist => this.getArtistIndex(artist)))]
|
||||||
|
}
|
||||||
|
|
||||||
|
createSortedAndFilteredList () {
|
||||||
|
var artistsSorted = this.items
|
||||||
|
if (this.options.hideSingles || this.options.hideSpotify || this.options.hideOther) {
|
||||||
|
artistsSorted = artistsSorted.filter(artist => this.isArtistVisible(artist))
|
||||||
|
}
|
||||||
|
if (this.options.sort === 'Recently added') {
|
||||||
|
artistsSorted = [...artistsSorted].sort((a, b) => b.time_added.localeCompare(a.time_added))
|
||||||
|
}
|
||||||
|
this.sortedAndFiltered = artistsSorted
|
||||||
|
}
|
||||||
|
|
||||||
|
createGroupedList () {
|
||||||
|
if (!this.options.group) {
|
||||||
|
this.grouped = {}
|
||||||
|
}
|
||||||
|
this.grouped = this.sortedAndFiltered.reduce((r, artist) => {
|
||||||
|
const idx = this.getArtistIndex(artist)
|
||||||
|
r[idx] = [...r[idx] || [], artist]
|
||||||
|
return r
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
}
|
@ -8,13 +8,18 @@ import './filter'
|
|||||||
import './progress'
|
import './progress'
|
||||||
import vClickOutside from 'v-click-outside'
|
import vClickOutside from 'v-click-outside'
|
||||||
import VueTinyLazyloadImg from 'vue-tiny-lazyload-img'
|
import VueTinyLazyloadImg from 'vue-tiny-lazyload-img'
|
||||||
|
import VueObserveVisibility from 'vue-observe-visibility'
|
||||||
|
import VueScrollTo from 'vue-scrollto'
|
||||||
import 'mdi/css/materialdesignicons.css'
|
import 'mdi/css/materialdesignicons.css'
|
||||||
import 'vue-range-slider/dist/vue-range-slider.css'
|
import 'vue-range-slider/dist/vue-range-slider.css'
|
||||||
import './mystyles.scss'
|
import './mystyles.scss'
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
Vue.use(vClickOutside)
|
Vue.use(vClickOutside)
|
||||||
Vue.use(VueTinyLazyloadImg)
|
Vue.use(VueTinyLazyloadImg)
|
||||||
|
Vue.use(VueObserveVisibility)
|
||||||
|
Vue.use(VueScrollTo)
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
new Vue({
|
new Vue({
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
@import 'bulma';
|
@import 'bulma';
|
||||||
|
@import '~bulma-switch';
|
||||||
|
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
@ -80,7 +81,9 @@ a.navbar-item {
|
|||||||
|
|
||||||
.fd-is-square .button {
|
.fd-is-square .button {
|
||||||
height: 27px;
|
height: 27px;
|
||||||
width: 27px;
|
min-width: 27px;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
padding-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fd-is-text-clipped {
|
.fd-is-text-clipped {
|
||||||
@ -115,6 +118,11 @@ section.hero + section.fd-content {
|
|||||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Set minimum height to hide "option" section */
|
||||||
|
.fd-content-with-option {
|
||||||
|
min-height: calc(100vh - 3.25rem - 3.25rem - 5rem);
|
||||||
|
}
|
||||||
|
|
||||||
/* Now playing page */
|
/* Now playing page */
|
||||||
.fd-is-fullheight {
|
.fd-is-fullheight {
|
||||||
height: calc(100vh - 3.25rem - 3.25rem);
|
height: calc(100vh - 3.25rem - 3.25rem);
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
<!-- Right side -->
|
<!-- Right side -->
|
||||||
<div class="level-right">
|
<div class="level-right">
|
||||||
<div v-if="library.updating"><a class="button is-small is-loading">Update</a></div>
|
<div v-if="library.updating"><a class="button is-small is-loading">Update</a></div>
|
||||||
<div v-else class="dropdown is-right" :class="{ 'is-active': show_update_dropdown }">
|
<div v-else class="dropdown is-right" :class="{ 'is-active': show_update_dropdown }" v-click-outside="onClickOutside">
|
||||||
<div class="dropdown-trigger">
|
<div class="dropdown-trigger">
|
||||||
<div class="buttons has-addons">
|
<div class="buttons has-addons">
|
||||||
<a @click="update" class="button is-small">Update</a>
|
<a @click="update" class="button is-small">Update</a>
|
||||||
@ -126,6 +126,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
onClickOutside (event) {
|
||||||
|
this.show_update_dropdown = false
|
||||||
|
},
|
||||||
|
|
||||||
update: function () {
|
update: function () {
|
||||||
this.show_update_dropdown = false
|
this.show_update_dropdown = false
|
||||||
webapi.library_update()
|
webapi.library_update()
|
||||||
|
@ -24,14 +24,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p>
|
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p>
|
||||||
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" @click="play_track(index)">
|
<list-tracks :tracks="tracks" :uris="album.uri"></list-tracks>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(track)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-track>
|
|
||||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
|
||||||
<modal-dialog-album :show="show_album_details_modal" :album="album" @close="show_album_details_modal = false" />
|
<modal-dialog-album :show="show_album_details_modal" :album="album" @close="show_album_details_modal = false" />
|
||||||
</template>
|
</template>
|
||||||
</content-with-hero>
|
</content-with-hero>
|
||||||
@ -40,8 +33,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHero from '@/templates/ContentWithHero'
|
import ContentWithHero from '@/templates/ContentWithHero'
|
||||||
import ListItemTrack from '@/components/ListItemTrack'
|
import ListTracks from '@/components/ListTracks'
|
||||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
|
||||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||||
import CoverArtwork from '@/components/CoverArtwork'
|
import CoverArtwork from '@/components/CoverArtwork'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
@ -63,16 +55,13 @@ const albumData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageAlbum',
|
name: 'PageAlbum',
|
||||||
mixins: [LoadDataBeforeEnterMixin(albumData)],
|
mixins: [LoadDataBeforeEnterMixin(albumData)],
|
||||||
components: { ContentWithHero, ListItemTrack, ModalDialogTrack, ModalDialogAlbum, CoverArtwork },
|
components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
album: {},
|
album: {},
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_track: {},
|
|
||||||
|
|
||||||
show_album_details_modal: false
|
show_album_details_modal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -85,15 +74,6 @@ export default {
|
|||||||
|
|
||||||
play: function () {
|
play: function () {
|
||||||
webapi.player_play_uri(this.album.uri, true)
|
webapi.player_play_uri(this.album.uri, true)
|
||||||
},
|
|
||||||
|
|
||||||
play_track: function (position) {
|
|
||||||
webapi.player_play_uri(this.album.uri, false, position)
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (track) {
|
|
||||||
this.selected_track = track
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,32 +4,40 @@
|
|||||||
|
|
||||||
<content-with-heading>
|
<content-with-heading>
|
||||||
<template slot="options">
|
<template slot="options">
|
||||||
<index-button-list :index="index_list"></index-button-list>
|
<index-button-list :index="albums_list.indexList"></index-button-list>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<p class="heading" style="margin-bottom: 24px;">Filter</p>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<input id="switchHideSingles" type="checkbox" name="switchHideSingles" class="switch" v-model="hide_singles">
|
||||||
|
<label for="switchHideSingles">Hide singles</label>
|
||||||
|
</div>
|
||||||
|
<p class="help">If active, hides singles and albums with tracks that only appear in playlists.</p>
|
||||||
|
</div>
|
||||||
|
<div class="field" v-if="spotify_enabled">
|
||||||
|
<div class="control">
|
||||||
|
<input id="switchHideSpotify" type="checkbox" name="switchHideSpotify" class="switch" v-model="hide_spotify">
|
||||||
|
<label for="switchHideSpotify">Hide albums from Spotify</label>
|
||||||
|
</div>
|
||||||
|
<p class="help">If active, hides albums that only appear in your Spotify library.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<p class="heading" style="margin-bottom: 24px;">Sort by</p>
|
||||||
|
<dropdown-menu v-model="sort" :options="sort_options"></dropdown-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template slot="heading-left">
|
<template slot="heading-left">
|
||||||
<p class="title is-4">Albums</p>
|
<p class="title is-4">Albums</p>
|
||||||
<p class="heading">{{ albums.total }} albums</p>
|
<p class="heading">{{ albums_list.sortedAndFiltered.length }} Albums</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="heading-right">
|
<template slot="heading-right">
|
||||||
<a class="button is-small" :class="{ 'is-info': hide_singles }" @click="update_hide_singles">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="mdi mdi-numeric-1-box-multiple-outline"></i>
|
|
||||||
</span>
|
|
||||||
<span>Hide singles</span>
|
|
||||||
</a>
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-album v-for="album in albums_filtered"
|
<list-albums :albums="albums_list"></list-albums>
|
||||||
:key="album.id"
|
|
||||||
:album="album"
|
|
||||||
@click="open_album(album)">
|
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(album)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-album>
|
|
||||||
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
@ -40,10 +48,11 @@ import { LoadDataBeforeEnterMixin } from './mixin'
|
|||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import TabsMusic from '@/components/TabsMusic'
|
import TabsMusic from '@/components/TabsMusic'
|
||||||
import IndexButtonList from '@/components/IndexButtonList'
|
import IndexButtonList from '@/components/IndexButtonList'
|
||||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
import ListAlbums from '@/components/ListAlbums'
|
||||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
import DropdownMenu from '@/components/DropdownMenu'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
|
import Albums from '@/lib/Albums'
|
||||||
|
|
||||||
const albumsData = {
|
const albumsData = {
|
||||||
load: function (to) {
|
load: function (to) {
|
||||||
@ -61,48 +70,60 @@ const albumsData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageAlbums',
|
name: 'PageAlbums',
|
||||||
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
||||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemAlbum, ModalDialogAlbum },
|
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListAlbums, DropdownMenu },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
albums: { items: [] },
|
albums: { items: [] },
|
||||||
index_list: [],
|
sort_options: ['Name', 'Recently added', 'Recently released']
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_album: {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hide_singles () {
|
albums_list () {
|
||||||
return this.$store.state.hide_singles
|
return new Albums(this.albums.items, {
|
||||||
|
hideSingles: this.hide_singles,
|
||||||
|
hideSpotify: this.hide_spotify,
|
||||||
|
sort: this.sort,
|
||||||
|
group: true
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
albums_filtered () {
|
spotify_enabled () {
|
||||||
return this.albums.items.filter(album => !this.hide_singles || album.track_count > 2)
|
return this.$store.state.spotify.webapi_token_valid
|
||||||
|
},
|
||||||
|
|
||||||
|
hide_singles: {
|
||||||
|
get () {
|
||||||
|
return this.$store.state.hide_singles
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
this.$store.commit(types.HIDE_SINGLES, value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hide_spotify: {
|
||||||
|
get () {
|
||||||
|
return this.$store.state.hide_spotify
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
this.$store.commit(types.HIDE_SPOTIFY, value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sort: {
|
||||||
|
get () {
|
||||||
|
return this.$store.state.albums_sort
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
this.$store.commit(types.ALBUMS_SORT, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update_hide_singles: function (e) {
|
scrollToTop: function () {
|
||||||
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles)
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
},
|
|
||||||
|
|
||||||
open_album: function (album) {
|
|
||||||
this.$router.push({ path: '/music/albums/' + album.id })
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (album) {
|
|
||||||
this.selected_album = album
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
'hide_singles' () {
|
|
||||||
this.index_list = [...new Set(this.albums.items
|
|
||||||
.filter(album => !this.$store.state.hide_singles || album.track_count > 2)
|
|
||||||
.map(album => album.name_sort.charAt(0).toUpperCase()))]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,14 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums | <a class="has-text-link" @click="open_tracks">{{ artist.track_count }} tracks</a></p>
|
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums | <a class="has-text-link" @click="open_tracks">{{ artist.track_count }} tracks</a></p>
|
||||||
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" @click="open_album(album)">
|
<list-albums :albums="albums.items"></list-albums>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(album)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-album>
|
|
||||||
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
|
|
||||||
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
|
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
@ -31,8 +24,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
import ListAlbums from '@/components/ListAlbums'
|
||||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
|
||||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
@ -53,16 +45,13 @@ const artistData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageArtist',
|
name: 'PageArtist',
|
||||||
mixins: [LoadDataBeforeEnterMixin(artistData)],
|
mixins: [LoadDataBeforeEnterMixin(artistData)],
|
||||||
components: { ContentWithHeading, ListItemAlbum, ModalDialogAlbum, ModalDialogArtist },
|
components: { ContentWithHeading, ListAlbums, ModalDialogArtist },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
artist: {},
|
artist: {},
|
||||||
albums: {},
|
albums: {},
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_album: {},
|
|
||||||
|
|
||||||
show_artist_details_modal: false
|
show_artist_details_modal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -74,15 +63,6 @@ export default {
|
|||||||
|
|
||||||
play: function () {
|
play: function () {
|
||||||
webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), true)
|
webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), true)
|
||||||
},
|
|
||||||
|
|
||||||
open_album: function (album) {
|
|
||||||
this.$router.push({ path: '/music/albums/' + album.id })
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (album) {
|
|
||||||
this.selected_album = album
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,14 +19,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_artist">{{ artist.album_count }} albums</a> | {{ artist.track_count }} tracks</p>
|
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_artist">{{ artist.album_count }} albums</a> | {{ artist.track_count }} tracks</p>
|
||||||
<list-item-track v-for="(track, index) in tracks.items" :key="track.id" :track="track" @click="play_track(index)">
|
<list-tracks :tracks="tracks.items" :uris="track_uris"></list-tracks>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(track)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-track>
|
|
||||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
|
||||||
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
|
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
@ -37,8 +30,7 @@
|
|||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import IndexButtonList from '@/components/IndexButtonList'
|
import IndexButtonList from '@/components/IndexButtonList'
|
||||||
import ListItemTrack from '@/components/ListItemTrack'
|
import ListTracks from '@/components/ListTracks'
|
||||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
|
||||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
@ -59,16 +51,13 @@ const tracksData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageArtistTracks',
|
name: 'PageArtistTracks',
|
||||||
mixins: [LoadDataBeforeEnterMixin(tracksData)],
|
mixins: [LoadDataBeforeEnterMixin(tracksData)],
|
||||||
components: { ContentWithHeading, ListItemTrack, IndexButtonList, ModalDialogTrack, ModalDialogArtist },
|
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogArtist },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
artist: {},
|
artist: {},
|
||||||
tracks: { items: [] },
|
tracks: { items: [] },
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_track: {},
|
|
||||||
|
|
||||||
show_artist_details_modal: false
|
show_artist_details_modal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -77,6 +66,10 @@ export default {
|
|||||||
index_list () {
|
index_list () {
|
||||||
return [...new Set(this.tracks.items
|
return [...new Set(this.tracks.items
|
||||||
.map(track => track.title_sort.charAt(0).toUpperCase()))]
|
.map(track => track.title_sort.charAt(0).toUpperCase()))]
|
||||||
|
},
|
||||||
|
|
||||||
|
track_uris () {
|
||||||
|
return this.tracks.items.map(a => a.uri).join(',')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -88,15 +81,6 @@ export default {
|
|||||||
|
|
||||||
play: function () {
|
play: function () {
|
||||||
webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), true)
|
webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), true)
|
||||||
},
|
|
||||||
|
|
||||||
play_track: function (position) {
|
|
||||||
webapi.player_play_uri(this.tracks.items.map(a => a.uri).join(','), false, position)
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (track) {
|
|
||||||
this.selected_track = track
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,32 +4,40 @@
|
|||||||
|
|
||||||
<content-with-heading>
|
<content-with-heading>
|
||||||
<template slot="options">
|
<template slot="options">
|
||||||
<index-button-list :index="index_list"></index-button-list>
|
<index-button-list :index="artists_list.indexList"></index-button-list>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<p class="heading" style="margin-bottom: 24px;">Filter</p>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<input id="switchHideSingles" type="checkbox" name="switchHideSingles" class="switch" v-model="hide_singles">
|
||||||
|
<label for="switchHideSingles">Hide singles</label>
|
||||||
|
</div>
|
||||||
|
<p class="help">If active, hides artists that only appear on singles or playlists.</p>
|
||||||
|
</div>
|
||||||
|
<div class="field" v-if="spotify_enabled">
|
||||||
|
<div class="control">
|
||||||
|
<input id="switchHideSpotify" type="checkbox" name="switchHideSpotify" class="switch" v-model="hide_spotify">
|
||||||
|
<label for="switchHideSpotify">Hide artists from Spotify</label>
|
||||||
|
</div>
|
||||||
|
<p class="help">If active, hides artists that only appear in your Spotify library.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<p class="heading" style="margin-bottom: 24px;">Sort by</p>
|
||||||
|
<dropdown-menu v-model="sort" :options="sort_options"></dropdown-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template slot="heading-left">
|
<template slot="heading-left">
|
||||||
<p class="title is-4">Artists</p>
|
<p class="title is-4">Artists</p>
|
||||||
<p class="heading">{{ artists.total }} artists</p>
|
<p class="heading">{{ artists_list.sortedAndFiltered.length }} Artists</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="heading-right">
|
<template slot="heading-right">
|
||||||
<a class="button is-small" :class="{ 'is-info': hide_singles }" @click="update_hide_singles">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="mdi mdi-numeric-1-box-multiple-outline"></i>
|
|
||||||
</span>
|
|
||||||
<span>Hide singles</span>
|
|
||||||
</a>
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-artist v-for="artist in artists_filtered"
|
<list-artists :artists="artists_list"></list-artists>
|
||||||
:key="artist.id"
|
|
||||||
:artist="artist"
|
|
||||||
@click="open_artist(artist)">
|
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(artist)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-artist>
|
|
||||||
<modal-dialog-artist :show="show_details_modal" :artist="selected_artist" @close="show_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
@ -40,14 +48,15 @@ import { LoadDataBeforeEnterMixin } from './mixin'
|
|||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import TabsMusic from '@/components/TabsMusic'
|
import TabsMusic from '@/components/TabsMusic'
|
||||||
import IndexButtonList from '@/components/IndexButtonList'
|
import IndexButtonList from '@/components/IndexButtonList'
|
||||||
import ListItemArtist from '@/components/ListItemArtist'
|
import ListArtists from '@/components/ListArtists'
|
||||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
import DropdownMenu from '@/components/DropdownMenu'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
|
import Artists from '@/lib/Artists'
|
||||||
|
|
||||||
const artistsData = {
|
const artistsData = {
|
||||||
load: function (to) {
|
load: function (to) {
|
||||||
return webapi.library_artists()
|
return webapi.library_artists('music')
|
||||||
},
|
},
|
||||||
|
|
||||||
set: function (vm, response) {
|
set: function (vm, response) {
|
||||||
@ -58,45 +67,60 @@ const artistsData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageArtists',
|
name: 'PageArtists',
|
||||||
mixins: [LoadDataBeforeEnterMixin(artistsData)],
|
mixins: [LoadDataBeforeEnterMixin(artistsData)],
|
||||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemArtist, ModalDialogArtist },
|
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListArtists, DropdownMenu },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
artists: { items: [] },
|
artists: { items: [] },
|
||||||
|
sort_options: ['Name', 'Recently added']
|
||||||
show_details_modal: false,
|
|
||||||
selected_artist: {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hide_singles () {
|
artists_list () {
|
||||||
return this.$store.state.hide_singles
|
return new Artists(this.artists.items, {
|
||||||
|
hideSingles: this.hide_singles,
|
||||||
|
hideSpotify: this.hide_spotify,
|
||||||
|
sort: this.sort,
|
||||||
|
group: true
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
index_list () {
|
spotify_enabled () {
|
||||||
return [...new Set(this.artists.items
|
return this.$store.state.spotify.webapi_token_valid
|
||||||
.filter(artist => !this.$store.state.hide_singles || artist.track_count > (artist.album_count * 2))
|
|
||||||
.map(artist => artist.name_sort.charAt(0).toUpperCase()))]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
artists_filtered () {
|
hide_singles: {
|
||||||
return this.artists.items.filter(artist => !this.hide_singles || artist.track_count > (artist.album_count * 2))
|
get () {
|
||||||
|
return this.$store.state.hide_singles
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
this.$store.commit(types.HIDE_SINGLES, value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hide_spotify: {
|
||||||
|
get () {
|
||||||
|
return this.$store.state.hide_spotify
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
this.$store.commit(types.HIDE_SPOTIFY, value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sort: {
|
||||||
|
get () {
|
||||||
|
return this.$store.state.artists_sort
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
this.$store.commit(types.ARTISTS_SORT, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update_hide_singles: function (e) {
|
scrollToTop: function () {
|
||||||
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles)
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
},
|
|
||||||
|
|
||||||
open_artist: function (artist) {
|
|
||||||
this.$router.push({ path: '/music/artists/' + artist.id })
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (artist) {
|
|
||||||
this.selected_artist = artist
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<content-with-heading>
|
|
||||||
<template slot="heading-left">
|
|
||||||
<p class="title is-4">Audiobooks</p>
|
|
||||||
<p class="heading">{{ albums.total }} audiobooks</p>
|
|
||||||
</template>
|
|
||||||
<template slot="content">
|
|
||||||
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" :media_kind="'audiobook'" @click="open_album(album)">
|
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(album)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-album>
|
|
||||||
<modal-dialog-album :show="show_details_modal" :album="selected_album" :media_kind="'audiobook'" @close="show_details_modal = false" />
|
|
||||||
</template>
|
|
||||||
</content-with-heading>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
|
||||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
|
||||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
|
||||||
import webapi from '@/webapi'
|
|
||||||
|
|
||||||
const albumsData = {
|
|
||||||
load: function (to) {
|
|
||||||
return webapi.library_albums('audiobook')
|
|
||||||
},
|
|
||||||
|
|
||||||
set: function (vm, response) {
|
|
||||||
vm.albums = response.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'PageAudiobooks',
|
|
||||||
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
|
||||||
components: { ContentWithHeading, ListItemAlbum, ModalDialogAlbum },
|
|
||||||
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
albums: {},
|
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_album: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
open_album: function (album) {
|
|
||||||
this.$router.push({ path: '/audiobooks/' + album.id })
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (album) {
|
|
||||||
this.selected_album = album
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
@ -1,43 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<content-with-heading>
|
<content-with-hero>
|
||||||
<template slot="heading-left">
|
<template slot="heading-left">
|
||||||
<div class="title is-4">{{ album.name }}</div>
|
<h1 class="title is-5">{{ album.name }}</h1>
|
||||||
<div class="title is-4 has-text-grey has-text-weight-normal">{{ album.artist }}</div>
|
<h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artist }}</a></h2>
|
||||||
</template>
|
|
||||||
<template slot="heading-right">
|
<div class="buttons fd-is-centered-mobile fd-has-margin-top">
|
||||||
<div class="buttons is-centered">
|
<a class="button is-small is-dark is-rounded" @click="play">
|
||||||
|
<span class="icon"><i class="mdi mdi-play"></i></span> <span>Play</span>
|
||||||
|
</a>
|
||||||
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
|
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
|
||||||
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="button is-small is-dark is-rounded" @click="play">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="mdi mdi-play"></i>
|
|
||||||
</span>
|
|
||||||
<span>Play</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template slot="heading-right">
|
||||||
|
<p class="image is-square fd-has-shadow fd-has-action">
|
||||||
|
<cover-artwork
|
||||||
|
:artwork_url="album.artwork_url"
|
||||||
|
:artist="album.artist"
|
||||||
|
:album="album.name"
|
||||||
|
@click="show_album_details_modal = true" />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
|
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p>
|
||||||
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" @click="play_track(index)">
|
<list-tracks :tracks="tracks" :uris="album.uri"></list-tracks>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(track)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-track>
|
|
||||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
|
||||||
<modal-dialog-album :show="show_album_details_modal" :album="album" :media_kind="'audiobook'" @close="show_album_details_modal = false" />
|
<modal-dialog-album :show="show_album_details_modal" :album="album" :media_kind="'audiobook'" @close="show_album_details_modal = false" />
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-hero>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHero from '@/templates/ContentWithHero'
|
||||||
import ListItemTrack from '@/components/ListItemTrack'
|
import ListTracks from '@/components/ListTracks'
|
||||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
|
||||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||||
|
import CoverArtwork from '@/components/CoverArtwork'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
const albumData = {
|
const albumData = {
|
||||||
@ -55,23 +53,25 @@ const albumData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PageAudiobook',
|
name: 'PageAudiobooksAlbum',
|
||||||
mixins: [LoadDataBeforeEnterMixin(albumData)],
|
mixins: [LoadDataBeforeEnterMixin(albumData)],
|
||||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
|
components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
album: {},
|
album: {},
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_track: {},
|
|
||||||
|
|
||||||
show_album_details_modal: false
|
show_album_details_modal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
open_artist: function () {
|
||||||
|
this.show_details_modal = false
|
||||||
|
this.$router.push({ path: '/audiobooks/artists/' + this.album.artist_id })
|
||||||
|
},
|
||||||
|
|
||||||
play: function () {
|
play: function () {
|
||||||
webapi.player_play_uri(this.album.uri, false)
|
webapi.player_play_uri(this.album.uri, false)
|
||||||
},
|
},
|
65
web-src/src/pages/PageAudiobooksAlbums.vue
Normal file
65
web-src/src/pages/PageAudiobooksAlbums.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<tabs-audiobooks></tabs-audiobooks>
|
||||||
|
|
||||||
|
<content-with-heading>
|
||||||
|
<template slot="options">
|
||||||
|
<index-button-list :index="albums_list.indexList"></index-button-list>
|
||||||
|
</template>
|
||||||
|
<template slot="heading-left">
|
||||||
|
<p class="title is-4">Audiobooks</p>
|
||||||
|
<p class="heading">{{ albums_list.sortedAndFiltered.length }} Audiobooks</p>
|
||||||
|
</template>
|
||||||
|
<template slot="content">
|
||||||
|
<list-albums :albums="albums_list"></list-albums>
|
||||||
|
</template>
|
||||||
|
</content-with-heading>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
|
import TabsAudiobooks from '@/components/TabsAudiobooks'
|
||||||
|
import IndexButtonList from '@/components/IndexButtonList'
|
||||||
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
|
import ListAlbums from '@/components/ListAlbums'
|
||||||
|
import webapi from '@/webapi'
|
||||||
|
import Albums from '@/lib/Albums'
|
||||||
|
|
||||||
|
const albumsData = {
|
||||||
|
load: function (to) {
|
||||||
|
return webapi.library_albums('audiobook')
|
||||||
|
},
|
||||||
|
|
||||||
|
set: function (vm, response) {
|
||||||
|
vm.albums = response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PageAudiobooksAlbums',
|
||||||
|
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
||||||
|
components: { TabsAudiobooks, ContentWithHeading, IndexButtonList, ListAlbums },
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
albums: { items: [] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
albums_list () {
|
||||||
|
return new Albums(this.albums.items, {
|
||||||
|
sort: 'Name',
|
||||||
|
group: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
68
web-src/src/pages/PageAudiobooksArtist.vue
Normal file
68
web-src/src/pages/PageAudiobooksArtist.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<content-with-heading>
|
||||||
|
<template slot="heading-left">
|
||||||
|
<p class="title is-4">{{ artist.name }}</p>
|
||||||
|
</template>
|
||||||
|
<template slot="heading-right">
|
||||||
|
<div class="buttons is-centered">
|
||||||
|
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true">
|
||||||
|
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
||||||
|
</a>
|
||||||
|
<a class="button is-small is-dark is-rounded" @click="play">
|
||||||
|
<span class="icon"><i class="mdi mdi-play"></i></span> <span>Shuffle</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template slot="content">
|
||||||
|
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums</p>
|
||||||
|
<list-albums :albums="albums.items"></list-albums>
|
||||||
|
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
|
||||||
|
</template>
|
||||||
|
</content-with-heading>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
|
import ListAlbums from '@/components/ListAlbums'
|
||||||
|
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||||
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
|
const artistData = {
|
||||||
|
load: function (to) {
|
||||||
|
return Promise.all([
|
||||||
|
webapi.library_artist(to.params.artist_id),
|
||||||
|
webapi.library_artist_albums(to.params.artist_id)
|
||||||
|
])
|
||||||
|
},
|
||||||
|
|
||||||
|
set: function (vm, response) {
|
||||||
|
vm.artist = response[0].data
|
||||||
|
vm.albums = response[1].data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PageAudiobooksArtist',
|
||||||
|
mixins: [LoadDataBeforeEnterMixin(artistData)],
|
||||||
|
components: { ContentWithHeading, ListAlbums, ModalDialogArtist },
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
artist: {},
|
||||||
|
albums: {},
|
||||||
|
|
||||||
|
show_artist_details_modal: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
play: function () {
|
||||||
|
webapi.player_play_uri(this.albums.items.map(a => a.uri).join(','), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
@ -1,35 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<tabs-music></tabs-music>
|
<tabs-audiobooks></tabs-audiobooks>
|
||||||
|
|
||||||
<content-with-heading>
|
<content-with-heading>
|
||||||
<template slot="options">
|
<template slot="options">
|
||||||
<index-button-list :index="index_list"></index-button-list>
|
<index-button-list :index="artists_list.indexList"></index-button-list>
|
||||||
</template>
|
</template>
|
||||||
<template slot="heading-left">
|
<template slot="heading-left">
|
||||||
<p class="title is-4">Artists</p>
|
<p class="title is-4">Authors</p>
|
||||||
<p class="heading">{{ artists.total }} artists</p>
|
<p class="heading">{{ artists_list.sortedAndFiltered.length }} Authors</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="heading-right">
|
<template slot="heading-right">
|
||||||
<a class="button is-small" :class="{ 'is-info': hide_singles }" @click="update_hide_singles">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="mdi mdi-numeric-1-box-multiple-outline"></i>
|
|
||||||
</span>
|
|
||||||
<span>Hide singles</span>
|
|
||||||
</a>
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-artist v-for="artist in artists_filtered"
|
<list-artists :artists="artists_list"></list-artists>
|
||||||
:key="artist.id"
|
|
||||||
:artist="artist"
|
|
||||||
@click="open_artist(artist)">
|
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(artist)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-artist>
|
|
||||||
<modal-dialog-artist :show="show_details_modal" :artist="selected_artist" @close="show_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
@ -38,16 +22,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import TabsMusic from '@/components/TabsMusic'
|
import TabsAudiobooks from '@/components/TabsAudiobooks'
|
||||||
import IndexButtonList from '@/components/IndexButtonList'
|
import IndexButtonList from '@/components/IndexButtonList'
|
||||||
import ListItemArtist from '@/components/ListItemArtist'
|
import ListArtists from '@/components/ListArtists'
|
||||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
import * as types from '@/store/mutation_types'
|
import Artists from '@/lib/Artists'
|
||||||
|
|
||||||
const artistsData = {
|
const artistsData = {
|
||||||
load: function (to) {
|
load: function (to) {
|
||||||
return webapi.library_artists()
|
return webapi.library_artists('audiobook')
|
||||||
},
|
},
|
||||||
|
|
||||||
set: function (vm, response) {
|
set: function (vm, response) {
|
||||||
@ -56,48 +39,26 @@ const artistsData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PageArtists',
|
name: 'PageAudiobooksArtists',
|
||||||
mixins: [LoadDataBeforeEnterMixin(artistsData)],
|
mixins: [LoadDataBeforeEnterMixin(artistsData)],
|
||||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemArtist, ModalDialogArtist },
|
components: { ContentWithHeading, TabsAudiobooks, IndexButtonList, ListArtists },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
artists: { items: [] },
|
artists: { items: [] }
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_artist: {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hide_singles () {
|
artists_list () {
|
||||||
return this.$store.state.hide_singles
|
return new Artists(this.artists.items, {
|
||||||
},
|
sort: 'Name',
|
||||||
|
group: true
|
||||||
index_list () {
|
})
|
||||||
return [...new Set(this.artists.items
|
|
||||||
.filter(artist => !this.$store.state.hide_singles || artist.track_count > (artist.album_count * 2))
|
|
||||||
.map(artist => artist.name_sort.charAt(0).toUpperCase()))]
|
|
||||||
},
|
|
||||||
|
|
||||||
artists_filtered () {
|
|
||||||
return this.artists.items.filter(artist => !this.hide_singles || artist.track_count > (artist.album_count * 2))
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update_hide_singles: function (e) {
|
|
||||||
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles)
|
|
||||||
},
|
|
||||||
|
|
||||||
open_artist: function (artist) {
|
|
||||||
this.$router.push({ path: '/music/artists/' + artist.id })
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (artist) {
|
|
||||||
this.selected_artist = artist
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -9,14 +9,7 @@
|
|||||||
<p class="heading">albums</p>
|
<p class="heading">albums</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-album v-for="album in recently_added.items" :key="album.id" :album="album" @click="open_album(album)">
|
<list-albums :albums="recently_added.items"></list-albums>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_album_dialog(album)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-album>
|
|
||||||
<modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<nav class="level">
|
<nav class="level">
|
||||||
@ -34,14 +27,7 @@
|
|||||||
<p class="heading">tracks</p>
|
<p class="heading">tracks</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-track v-for="track in recently_played.items" :key="track.id" :track="track" @click="play_track(track)">
|
<list-tracks :tracks="recently_played.items"></list-tracks>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_track_dialog(track)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-track>
|
|
||||||
<modal-dialog-track :show="show_track_details_modal" :track="selected_track" @close="show_track_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<nav class="level">
|
<nav class="level">
|
||||||
@ -58,10 +44,8 @@
|
|||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import TabsMusic from '@/components/TabsMusic'
|
import TabsMusic from '@/components/TabsMusic'
|
||||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
import ListAlbums from '@/components/ListAlbums'
|
||||||
import ListItemTrack from '@/components/ListItemTrack'
|
import ListTracks from '@/components/ListTracks'
|
||||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
|
||||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
const browseData = {
|
const browseData = {
|
||||||
@ -81,7 +65,7 @@ const browseData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageBrowse',
|
name: 'PageBrowse',
|
||||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||||
components: { ContentWithHeading, TabsMusic, ListItemAlbum, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
|
components: { ContentWithHeading, TabsMusic, ListAlbums, ListTracks },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -89,34 +73,13 @@ export default {
|
|||||||
recently_played: {},
|
recently_played: {},
|
||||||
|
|
||||||
show_track_details_modal: false,
|
show_track_details_modal: false,
|
||||||
selected_track: {},
|
selected_track: {}
|
||||||
|
|
||||||
show_album_details_modal: false,
|
|
||||||
selected_album: {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
open_browse: function (type) {
|
open_browse: function (type) {
|
||||||
this.$router.push({ path: '/music/browse/' + type })
|
this.$router.push({ path: '/music/browse/' + type })
|
||||||
},
|
|
||||||
|
|
||||||
open_track_dialog: function (track) {
|
|
||||||
this.selected_track = track
|
|
||||||
this.show_track_details_modal = true
|
|
||||||
},
|
|
||||||
|
|
||||||
open_album: function (album) {
|
|
||||||
this.$router.push({ path: '/music/albums/' + album.id })
|
|
||||||
},
|
|
||||||
|
|
||||||
open_album_dialog: function (album) {
|
|
||||||
this.selected_album = album
|
|
||||||
this.show_album_details_modal = true
|
|
||||||
},
|
|
||||||
|
|
||||||
play_track: function (track) {
|
|
||||||
webapi.player_play_uri(track.uri, false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,7 @@
|
|||||||
<p class="heading">albums</p>
|
<p class="heading">albums</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-album v-for="album in recently_added.items" :key="album.id" :album="album" @click="open_album(album)">
|
<list-albums :albums="recently_added.items"></list-albums>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(album)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-album>
|
|
||||||
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
@ -25,8 +18,7 @@
|
|||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import TabsMusic from '@/components/TabsMusic'
|
import TabsMusic from '@/components/TabsMusic'
|
||||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
import ListAlbums from '@/components/ListAlbums'
|
||||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
const browseData = {
|
const browseData = {
|
||||||
@ -46,25 +38,11 @@ const browseData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageBrowseType',
|
name: 'PageBrowseType',
|
||||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||||
components: { ContentWithHeading, TabsMusic, ListItemAlbum, ModalDialogAlbum },
|
components: { ContentWithHeading, TabsMusic, ListAlbums },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
recently_added: {},
|
recently_added: {}
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_album: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
open_album: function (album) {
|
|
||||||
this.$router.push({ path: '/music/albums/' + album.id })
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (album) {
|
|
||||||
this.selected_album = album
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,7 @@
|
|||||||
<p class="heading">tracks</p>
|
<p class="heading">tracks</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-track v-for="track in recently_played.items" :key="track.id" :track="track" @click="play_track(track)">
|
<list-tracks :tracks="recently_played.items"></list-tracks>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(track)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-track>
|
|
||||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
@ -25,8 +18,7 @@
|
|||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import TabsMusic from '@/components/TabsMusic'
|
import TabsMusic from '@/components/TabsMusic'
|
||||||
import ListItemTrack from '@/components/ListItemTrack'
|
import ListTracks from '@/components/ListTracks'
|
||||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
const browseData = {
|
const browseData = {
|
||||||
@ -46,25 +38,11 @@ const browseData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageBrowseType',
|
name: 'PageBrowseType',
|
||||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||||
components: { ContentWithHeading, TabsMusic, ListItemTrack, ModalDialogTrack },
|
components: { ContentWithHeading, TabsMusic, ListTracks },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
recently_played: {},
|
recently_played: {}
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_track: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
open_dialog: function (track) {
|
|
||||||
this.selected_track = track
|
|
||||||
this.show_details_modal = true
|
|
||||||
},
|
|
||||||
|
|
||||||
play_track: function (track) {
|
|
||||||
webapi.player_play_uri(track.uri, false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,14 +19,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<p class="heading has-text-centered-mobile">{{ genre_albums.total }} albums | <a class="has-text-link" @click="open_tracks">tracks</a></p>
|
<p class="heading has-text-centered-mobile">{{ genre_albums.total }} albums | <a class="has-text-link" @click="open_tracks">tracks</a></p>
|
||||||
<list-item-albums v-for="album in genre_albums.items" :key="album.id" :album="album" @click="open_album(album)">
|
<list-albums :albums="genre_albums.items"></list-albums>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(album)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-albums>
|
|
||||||
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
|
|
||||||
<modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': name }" @close="show_genre_details_modal = false" />
|
<modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': name }" @close="show_genre_details_modal = false" />
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
@ -37,8 +30,7 @@
|
|||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import IndexButtonList from '@/components/IndexButtonList'
|
import IndexButtonList from '@/components/IndexButtonList'
|
||||||
import ListItemAlbums from '@/components/ListItemAlbum'
|
import ListAlbums from '@/components/ListAlbums'
|
||||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
|
||||||
import ModalDialogGenre from '@/components/ModalDialogGenre'
|
import ModalDialogGenre from '@/components/ModalDialogGenre'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
@ -56,16 +48,13 @@ const genreData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageGenre',
|
name: 'PageGenre',
|
||||||
mixins: [LoadDataBeforeEnterMixin(genreData)],
|
mixins: [LoadDataBeforeEnterMixin(genreData)],
|
||||||
components: { ContentWithHeading, IndexButtonList, ListItemAlbums, ModalDialogAlbum, ModalDialogGenre },
|
components: { ContentWithHeading, IndexButtonList, ListAlbums, ModalDialogGenre },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
genre_albums: { items: [] },
|
genre_albums: { items: [] },
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_album: {},
|
|
||||||
|
|
||||||
show_genre_details_modal: false
|
show_genre_details_modal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -87,10 +76,6 @@ export default {
|
|||||||
webapi.player_play_expression('genre is "' + this.name + '" and media_kind is music', true)
|
webapi.player_play_expression('genre is "' + this.name + '" and media_kind is music', true)
|
||||||
},
|
},
|
||||||
|
|
||||||
open_album: function (album) {
|
|
||||||
this.$router.push({ path: '/music/albums/' + album.id })
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (album) {
|
open_dialog: function (album) {
|
||||||
this.selected_album = album
|
this.selected_album = album
|
||||||
this.show_details_modal = true
|
this.show_details_modal = true
|
||||||
|
@ -19,14 +19,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_genre">albums</a> | {{ tracks.total }} tracks</p>
|
<p class="heading has-text-centered-mobile"><a class="has-text-link" @click="open_genre">albums</a> | {{ tracks.total }} tracks</p>
|
||||||
<list-item-track v-for="(track, index) in tracks.items" :key="track.id" :track="track" @click="play_track(index)">
|
<list-tracks :tracks="tracks.items" :expression="expression"></list-tracks>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(track)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-track>
|
|
||||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
|
||||||
<modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': genre }" @close="show_genre_details_modal = false" />
|
<modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': genre }" @close="show_genre_details_modal = false" />
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
@ -37,8 +30,7 @@
|
|||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import IndexButtonList from '@/components/IndexButtonList'
|
import IndexButtonList from '@/components/IndexButtonList'
|
||||||
import ListItemTrack from '@/components/ListItemTrack'
|
import ListTracks from '@/components/ListTracks'
|
||||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
|
||||||
import ModalDialogGenre from '@/components/ModalDialogGenre'
|
import ModalDialogGenre from '@/components/ModalDialogGenre'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
@ -56,16 +48,13 @@ const tracksData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageGenreTracks',
|
name: 'PageGenreTracks',
|
||||||
mixins: [LoadDataBeforeEnterMixin(tracksData)],
|
mixins: [LoadDataBeforeEnterMixin(tracksData)],
|
||||||
components: { ContentWithHeading, ListItemTrack, IndexButtonList, ModalDialogTrack, ModalDialogGenre },
|
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogGenre },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
tracks: { items: [] },
|
tracks: { items: [] },
|
||||||
genre: '',
|
genre: '',
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_track: {},
|
|
||||||
|
|
||||||
show_genre_details_modal: false
|
show_genre_details_modal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -74,6 +63,10 @@ export default {
|
|||||||
index_list () {
|
index_list () {
|
||||||
return [...new Set(this.tracks.items
|
return [...new Set(this.tracks.items
|
||||||
.map(track => track.title_sort.charAt(0).toUpperCase()))]
|
.map(track => track.title_sort.charAt(0).toUpperCase()))]
|
||||||
|
},
|
||||||
|
|
||||||
|
expression () {
|
||||||
|
return 'genre is "' + this.genre + '" and media_kind is music'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -84,16 +77,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
play: function () {
|
play: function () {
|
||||||
webapi.player_play_expression('genre is "' + this.genre + '" and media_kind is music', true)
|
webapi.player_play_expression(this.expression, true)
|
||||||
},
|
|
||||||
|
|
||||||
play_track: function (position) {
|
|
||||||
webapi.player_play_expression('genre is "' + this.genre + '" and media_kind is music', false, position)
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (track) {
|
|
||||||
this.selected_track = track
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<p class="heading has-text-centered-mobile">{{ tracks.length }} tracks</p>
|
<p class="heading has-text-centered-mobile">{{ tracks.length }} tracks</p>
|
||||||
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" @click="play_track(index)">
|
<list-tracks :tracks="tracks" :uris="uris"></list-tracks>
|
||||||
<template slot="actions">
|
<modal-dialog-playlist :show="show_playlist_details_modal" :playlist="playlist" :tracks="playlist.random ? tracks : undefined" @close="show_playlist_details_modal = false" />
|
||||||
<a @click="open_dialog(track)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-track>
|
|
||||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
|
||||||
<modal-dialog-playlist :show="show_playlist_details_modal" :playlist="playlist" @close="show_playlist_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</template>
|
</template>
|
||||||
@ -31,8 +24,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import ListItemTrack from '@/components/ListItemTrack'
|
import ListTracks from '@/components/ListTracks'
|
||||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
|
||||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
@ -53,32 +45,29 @@ const playlistData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PagePlaylist',
|
name: 'PagePlaylist',
|
||||||
mixins: [LoadDataBeforeEnterMixin(playlistData)],
|
mixins: [LoadDataBeforeEnterMixin(playlistData)],
|
||||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogPlaylist },
|
components: { ContentWithHeading, ListTracks, ModalDialogPlaylist },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
playlist: {},
|
playlist: {},
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_track: {},
|
|
||||||
|
|
||||||
show_playlist_details_modal: false
|
show_playlist_details_modal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
uris () {
|
||||||
|
if (this.playlist.random) {
|
||||||
|
return this.tracks.map(a => a.uri).join(',')
|
||||||
|
}
|
||||||
|
return this.playlist.uri
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
play: function () {
|
play: function () {
|
||||||
webapi.player_play_uri(this.playlist.uri, true)
|
webapi.player_play_uri(this.uris, true)
|
||||||
},
|
|
||||||
|
|
||||||
play_track: function (position) {
|
|
||||||
webapi.player_play_uri(this.playlist.uri, false, position)
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (track) {
|
|
||||||
this.selected_track = track
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,19 +5,7 @@
|
|||||||
<p class="heading">{{ playlists.total }} playlists</p>
|
<p class="heading">{{ playlists.total }} playlists</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
|
<list-playlists :playlists="playlists.items"></list-playlists>
|
||||||
<template slot="icon">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="mdi" :class="{ 'mdi-library-music': playlist.type !== 'folder', 'mdi-rss': playlist.type === 'rss', 'mdi-folder': playlist.type === 'folder' }"></i>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(playlist)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-playlist>
|
|
||||||
<modal-dialog-playlist :show="show_details_modal" :playlist="selected_playlist" @close="show_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</template>
|
</template>
|
||||||
@ -25,8 +13,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import ListItemPlaylist from '@/components/ListItemPlaylist'
|
import ListPlaylists from '@/components/ListPlaylists'
|
||||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
const playlistsData = {
|
const playlistsData = {
|
||||||
@ -46,30 +33,12 @@ const playlistsData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PagePlaylists',
|
name: 'PagePlaylists',
|
||||||
mixins: [LoadDataBeforeEnterMixin(playlistsData)],
|
mixins: [LoadDataBeforeEnterMixin(playlistsData)],
|
||||||
components: { ContentWithHeading, ListItemPlaylist, ModalDialogPlaylist },
|
components: { ContentWithHeading, ListPlaylists },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
playlist: {},
|
playlist: {},
|
||||||
playlists: {},
|
playlists: {}
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_playlist: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
open_playlist: function (playlist) {
|
|
||||||
if (playlist.type !== 'folder') {
|
|
||||||
this.$router.push({ path: '/playlists/' + playlist.id + '/tracks' })
|
|
||||||
} else {
|
|
||||||
this.$router.push({ path: '/playlists/' + playlist.id })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (playlist) {
|
|
||||||
this.selected_playlist = playlist
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,35 +52,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" :media_kind="'podcast'" @click="open_album(album)">
|
<list-albums :albums="albums.items"
|
||||||
<template slot="actions">
|
@play_count_changed="reload_new_episodes()"
|
||||||
<a @click="open_album_dialog(album)">
|
@podcast-deleted="reload_podcasts()">
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
</list-albums>
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-album>
|
|
||||||
<modal-dialog-album
|
|
||||||
:show="show_album_details_modal"
|
|
||||||
:album="selected_album"
|
|
||||||
:media_kind="'podcast'"
|
|
||||||
@close="show_album_details_modal = false"
|
|
||||||
@play_count_changed="reload_new_episodes"
|
|
||||||
@remove_podcast="open_remove_podcast_dialog" />
|
|
||||||
<modal-dialog
|
|
||||||
:show="show_remove_podcast_modal"
|
|
||||||
title="Remove podcast"
|
|
||||||
delete_action="Remove"
|
|
||||||
@close="show_remove_podcast_modal = false"
|
|
||||||
@delete="remove_podcast">
|
|
||||||
<template slot="modal-content">
|
|
||||||
<p>Permanently remove this podcast from your library?</p>
|
|
||||||
<p class="is-size-7">(This will also remove the RSS playlist <b>{{ rss_playlist_to_remove.name }}</b>.)</p>
|
|
||||||
</template>
|
|
||||||
</modal-dialog>
|
|
||||||
<modal-dialog-add-rss
|
<modal-dialog-add-rss
|
||||||
:show="show_url_modal"
|
:show="show_url_modal"
|
||||||
@close="show_url_modal = false"
|
@close="show_url_modal = false"
|
||||||
@podcast_added="reload_podcasts" />
|
@podcast_added="reload_podcasts()" />
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
@ -90,11 +69,9 @@
|
|||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import ListItemTrack from '@/components/ListItemTrack'
|
import ListItemTrack from '@/components/ListItemTrack'
|
||||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
import ListAlbums from '@/components/ListAlbums'
|
||||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
|
||||||
import ModalDialogAddRss from '@/components/ModalDialogAddRss'
|
import ModalDialogAddRss from '@/components/ModalDialogAddRss'
|
||||||
import ModalDialog from '@/components/ModalDialog'
|
|
||||||
import RangeSlider from 'vue-range-slider'
|
import RangeSlider from 'vue-range-slider'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
@ -115,31 +92,21 @@ const albumsData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PagePodcasts',
|
name: 'PagePodcasts',
|
||||||
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
||||||
components: { ContentWithHeading, ListItemTrack, ListItemAlbum, ModalDialogTrack, ModalDialogAlbum, ModalDialogAddRss, ModalDialog, RangeSlider },
|
components: { ContentWithHeading, ListItemTrack, ListAlbums, ModalDialogTrack, ModalDialogAddRss, RangeSlider },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
albums: {},
|
albums: {},
|
||||||
new_episodes: { items: [] },
|
new_episodes: { items: [] },
|
||||||
|
|
||||||
show_album_details_modal: false,
|
|
||||||
selected_album: {},
|
|
||||||
|
|
||||||
show_url_modal: false,
|
show_url_modal: false,
|
||||||
|
|
||||||
show_track_details_modal: false,
|
show_track_details_modal: false,
|
||||||
selected_track: {},
|
selected_track: {}
|
||||||
|
|
||||||
show_remove_podcast_modal: false,
|
|
||||||
rss_playlist_to_remove: {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
open_album: function (album) {
|
|
||||||
this.$router.push({ path: '/podcasts/' + album.id })
|
|
||||||
},
|
|
||||||
|
|
||||||
play_track: function (track) {
|
play_track: function (track) {
|
||||||
webapi.player_play_uri(track.uri, false)
|
webapi.player_play_uri(track.uri, false)
|
||||||
},
|
},
|
||||||
@ -149,11 +116,6 @@ export default {
|
|||||||
this.show_track_details_modal = true
|
this.show_track_details_modal = true
|
||||||
},
|
},
|
||||||
|
|
||||||
open_album_dialog: function (album) {
|
|
||||||
this.selected_album = album
|
|
||||||
this.show_album_details_modal = true
|
|
||||||
},
|
|
||||||
|
|
||||||
mark_all_played: function () {
|
mark_all_played: function () {
|
||||||
this.new_episodes.items.forEach(ep => {
|
this.new_episodes.items.forEach(ep => {
|
||||||
webapi.library_track_update(ep.id, { play_count: 'increment' })
|
webapi.library_track_update(ep.id, { play_count: 'increment' })
|
||||||
@ -165,29 +127,6 @@ export default {
|
|||||||
this.show_url_modal = true
|
this.show_url_modal = true
|
||||||
},
|
},
|
||||||
|
|
||||||
open_remove_podcast_dialog: function () {
|
|
||||||
this.show_album_details_modal = false
|
|
||||||
webapi.library_album_tracks(this.selected_album.id, { limit: 1 }).then(({ data }) => {
|
|
||||||
webapi.library_track_playlists(data.items[0].id).then(({ data }) => {
|
|
||||||
const rssPlaylists = data.items.filter(pl => pl.type === 'rss')
|
|
||||||
if (rssPlaylists.length !== 1) {
|
|
||||||
this.$store.dispatch('add_notification', { text: 'Podcast cannot be removed. Probably it was not added as an RSS playlist.', type: 'danger' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rss_playlist_to_remove = rssPlaylists[0]
|
|
||||||
this.show_remove_podcast_modal = true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
remove_podcast: function () {
|
|
||||||
this.show_remove_podcast_modal = false
|
|
||||||
webapi.library_playlist_delete(this.rss_playlist_to_remove.id).then(() => {
|
|
||||||
this.reload_podcasts()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
reload_new_episodes: function () {
|
reload_new_episodes: function () {
|
||||||
webapi.library_podcasts_new_episodes().then(({ data }) => {
|
webapi.library_podcasts_new_episodes().then(({ data }) => {
|
||||||
this.new_episodes = data.tracks
|
this.new_episodes = data.tracks
|
||||||
|
@ -1,21 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<tabs-music></tabs-music>
|
|
||||||
|
|
||||||
<content-with-heading>
|
<content-with-heading>
|
||||||
<template slot="heading-left">
|
<template slot="heading-left">
|
||||||
<p class="title is-4">Radio</p>
|
<p class="title is-4">Radio</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<p class="heading has-text-centered-mobile">{{ tracks.total }} tracks</p>
|
<p class="heading has-text-centered-mobile">{{ tracks.total }} tracks</p>
|
||||||
<list-item-track v-for="track in tracks.items" :key="track.id" :track="track" @click="play_track(track)">
|
<list-tracks :tracks="tracks.items"></list-tracks>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_dialog(track)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-track>
|
|
||||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
@ -23,10 +14,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import TabsMusic from '@/components/TabsMusic'
|
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import ListItemTrack from '@/components/ListItemTrack'
|
import ListTracks from '@/components/ListTracks'
|
||||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
const streamsData = {
|
const streamsData = {
|
||||||
@ -42,25 +31,11 @@ const streamsData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageRadioStreams',
|
name: 'PageRadioStreams',
|
||||||
mixins: [LoadDataBeforeEnterMixin(streamsData)],
|
mixins: [LoadDataBeforeEnterMixin(streamsData)],
|
||||||
components: { TabsMusic, ContentWithHeading, ListItemTrack, ModalDialogTrack },
|
components: { ContentWithHeading, ListTracks },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
tracks: { items: [] },
|
tracks: { items: [] }
|
||||||
|
|
||||||
show_details_modal: false,
|
|
||||||
selected_track: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
play_track: function (track) {
|
|
||||||
webapi.player_play_uri(track.uri, false)
|
|
||||||
},
|
|
||||||
|
|
||||||
open_dialog: function (track) {
|
|
||||||
this.selected_track = track
|
|
||||||
this.show_details_modal = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,19 +34,12 @@
|
|||||||
<p class="title is-4">Tracks</p>
|
<p class="title is-4">Tracks</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-track v-for="track in tracks.items" :key="track.id" :track="track" @click="play_track(track)">
|
<list-tracks :tracks="tracks.items"></list-tracks>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_track_dialog(track)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-track>
|
|
||||||
<modal-dialog-track :show="show_track_details_modal" :track="selected_track" @close="show_track_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<nav v-if="show_all_tracks_button" class="level">
|
<nav v-if="show_all_tracks_button" class="level">
|
||||||
<p class="level-item">
|
<p class="level-item">
|
||||||
<a class="button is-light is-small is-rounded" v-on:click="open_search_tracks">Show all {{ tracks.total }} tracks</a>
|
<a class="button is-light is-small is-rounded" v-on:click="open_search_tracks">Show all {{ tracks.total.toLocaleString() }} tracks</a>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
<p v-if="!tracks.total">No results</p>
|
<p v-if="!tracks.total">No results</p>
|
||||||
@ -59,19 +52,12 @@
|
|||||||
<p class="title is-4">Artists</p>
|
<p class="title is-4">Artists</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-artist v-for="artist in artists.items" :key="artist.id" :artist="artist" @click="open_artist(artist)">
|
<list-artists :artists="artists.items"></list-artists>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_artist_dialog(artist)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-artist>
|
|
||||||
<modal-dialog-artist :show="show_artist_details_modal" :artist="selected_artist" @close="show_artist_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<nav v-if="show_all_artists_button" class="level">
|
<nav v-if="show_all_artists_button" class="level">
|
||||||
<p class="level-item">
|
<p class="level-item">
|
||||||
<a class="button is-light is-small is-rounded" v-on:click="open_search_artists">Show all {{ artists.total }} artists</a>
|
<a class="button is-light is-small is-rounded" v-on:click="open_search_artists">Show all {{ artists.total.toLocaleString() }} artists</a>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
<p v-if="!artists.total">No results</p>
|
<p v-if="!artists.total">No results</p>
|
||||||
@ -84,19 +70,12 @@
|
|||||||
<p class="title is-4">Albums</p>
|
<p class="title is-4">Albums</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" @click="open_album(album)">
|
<list-albums :albums="albums.items"></list-albums>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_album_dialog(album)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-album>
|
|
||||||
<modal-dialog-album :show="show_album_details_modal" :album="selected_album" @close="show_album_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<nav v-if="show_all_albums_button" class="level">
|
<nav v-if="show_all_albums_button" class="level">
|
||||||
<p class="level-item">
|
<p class="level-item">
|
||||||
<a class="button is-light is-small is-rounded" v-on:click="open_search_albums">Show all {{ albums.total }} albums</a>
|
<a class="button is-light is-small is-rounded" v-on:click="open_search_albums">Show all {{ albums.total.toLocaleString() }} albums</a>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
<p v-if="!albums.total">No results</p>
|
<p v-if="!albums.total">No results</p>
|
||||||
@ -109,44 +88,69 @@
|
|||||||
<p class="title is-4">Playlists</p>
|
<p class="title is-4">Playlists</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
|
<list-playlists :playlists="playlists.items"></list-playlists>
|
||||||
<template slot="actions">
|
|
||||||
<a @click="open_playlist_dialog(playlist)">
|
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</list-item-playlist>
|
|
||||||
<modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" />
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<nav v-if="show_all_playlists_button" class="level">
|
<nav v-if="show_all_playlists_button" class="level">
|
||||||
<p class="level-item">
|
<p class="level-item">
|
||||||
<a class="button is-light is-small is-rounded" v-on:click="open_search_playlists">Show all {{ playlists.total }} playlists</a>
|
<a class="button is-light is-small is-rounded" v-on:click="open_search_playlists">Show all {{ playlists.total.toLocaleString() }} playlists</a>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
<p v-if="!playlists.total">No results</p>
|
<p v-if="!playlists.total">No results</p>
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
|
|
||||||
|
<!-- Podcasts -->
|
||||||
|
<content-with-heading v-if="show_podcasts">
|
||||||
|
<template slot="heading-left">
|
||||||
|
<p class="title is-4">Podcasts</p>
|
||||||
|
</template>
|
||||||
|
<template slot="content">
|
||||||
|
<list-albums :albums="podcasts.items"></list-albums>
|
||||||
|
</template>
|
||||||
|
<template slot="footer">
|
||||||
|
<nav v-if="show_all_podcasts_button" class="level">
|
||||||
|
<p class="level-item">
|
||||||
|
<a class="button is-light is-small is-rounded" v-on:click="open_search_podcasts">Show all {{ podcasts.total.toLocaleString() }} podcasts</a>
|
||||||
|
</p>
|
||||||
|
</nav>
|
||||||
|
<p v-if="!podcasts.total">No results</p>
|
||||||
|
</template>
|
||||||
|
</content-with-heading>
|
||||||
|
|
||||||
|
<!-- Audiobooks -->
|
||||||
|
<content-with-heading v-if="show_audiobooks">
|
||||||
|
<template slot="heading-left">
|
||||||
|
<p class="title is-4">Audiobooks</p>
|
||||||
|
</template>
|
||||||
|
<template slot="content">
|
||||||
|
<list-albums :albums="audiobooks.items"></list-albums>
|
||||||
|
</template>
|
||||||
|
<template slot="footer">
|
||||||
|
<nav v-if="show_all_audiobooks_button" class="level">
|
||||||
|
<p class="level-item">
|
||||||
|
<a class="button is-light is-small is-rounded" v-on:click="open_search_audiobooks">Show all {{ audiobooks.total.toLocaleString() }} audiobooks</a>
|
||||||
|
</p>
|
||||||
|
</nav>
|
||||||
|
<p v-if="!audiobooks.total">No results</p>
|
||||||
|
</template>
|
||||||
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import TabsSearch from '@/components/TabsSearch'
|
import TabsSearch from '@/components/TabsSearch'
|
||||||
import ListItemTrack from '@/components/ListItemTrack'
|
import ListTracks from '@/components/ListTracks'
|
||||||
import ListItemArtist from '@/components/ListItemArtist'
|
import ListArtists from '@/components/ListArtists'
|
||||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
import ListAlbums from '@/components/ListAlbums'
|
||||||
import ListItemPlaylist from '@/components/ListItemPlaylist'
|
import ListPlaylists from '@/components/ListPlaylists'
|
||||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
|
||||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
|
||||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
|
||||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PageSearch',
|
name: 'PageSearch',
|
||||||
components: { ContentWithHeading, TabsSearch, ListItemTrack, ListItemArtist, ListItemAlbum, ListItemPlaylist, ModalDialogTrack, ModalDialogAlbum, ModalDialogArtist, ModalDialogPlaylist },
|
components: { ContentWithHeading, TabsSearch, ListTracks, ListArtists, ListAlbums, ListPlaylists },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -156,18 +160,8 @@ export default {
|
|||||||
artists: { items: [], total: 0 },
|
artists: { items: [], total: 0 },
|
||||||
albums: { items: [], total: 0 },
|
albums: { items: [], total: 0 },
|
||||||
playlists: { items: [], total: 0 },
|
playlists: { items: [], total: 0 },
|
||||||
|
audiobooks: { items: [], total: 0 },
|
||||||
show_track_details_modal: false,
|
podcasts: { items: [], total: 0 }
|
||||||
selected_track: {},
|
|
||||||
|
|
||||||
show_album_details_modal: false,
|
|
||||||
selected_album: {},
|
|
||||||
|
|
||||||
show_artist_details_modal: false,
|
|
||||||
selected_artist: {},
|
|
||||||
|
|
||||||
show_playlist_details_modal: false,
|
|
||||||
selected_playlist: {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -202,6 +196,24 @@ export default {
|
|||||||
},
|
},
|
||||||
show_all_playlists_button () {
|
show_all_playlists_button () {
|
||||||
return this.playlists.total > this.playlists.items.length
|
return this.playlists.total > this.playlists.items.length
|
||||||
|
},
|
||||||
|
|
||||||
|
show_audiobooks () {
|
||||||
|
return this.$route.query.type && this.$route.query.type.includes('audiobook')
|
||||||
|
},
|
||||||
|
show_all_audiobooks_button () {
|
||||||
|
return this.audiobooks.total > this.audiobooks.items.length
|
||||||
|
},
|
||||||
|
|
||||||
|
show_podcasts () {
|
||||||
|
return this.$route.query.type && this.$route.query.type.includes('podcast')
|
||||||
|
},
|
||||||
|
show_all_podcasts_button () {
|
||||||
|
return this.podcasts.total > this.podcasts.items.length
|
||||||
|
},
|
||||||
|
|
||||||
|
is_visible_artwork () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_cover_artwork_in_album_lists').value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -213,20 +225,31 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.searchMusic(route.query)
|
||||||
|
this.searchAudiobooks(route.query)
|
||||||
|
this.searchPodcasts(route.query)
|
||||||
|
this.$store.commit(types.ADD_RECENT_SEARCH, route.query.query)
|
||||||
|
},
|
||||||
|
|
||||||
|
searchMusic: function (query) {
|
||||||
|
if (query.type.indexOf('track') < 0 && query.type.indexOf('artist') < 0 && query.type.indexOf('album') < 0 && query.type.indexOf('playlist') < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var searchParams = {
|
var searchParams = {
|
||||||
type: route.query.type,
|
type: query.type,
|
||||||
media_kind: 'music'
|
media_kind: 'music'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.query.query.startsWith('query:')) {
|
if (query.query.startsWith('query:')) {
|
||||||
searchParams.expression = route.query.query.replace(/^query:/, '').trim()
|
searchParams.expression = query.query.replace(/^query:/, '').trim()
|
||||||
} else {
|
} else {
|
||||||
searchParams.query = route.query.query
|
searchParams.query = query.query
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.query.limit) {
|
if (query.limit) {
|
||||||
searchParams.limit = route.query.limit
|
searchParams.limit = query.limit
|
||||||
searchParams.offset = route.query.offset
|
searchParams.offset = query.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
webapi.search(searchParams).then(({ data }) => {
|
webapi.search(searchParams).then(({ data }) => {
|
||||||
@ -234,8 +257,58 @@ export default {
|
|||||||
this.artists = data.artists ? data.artists : { items: [], total: 0 }
|
this.artists = data.artists ? data.artists : { items: [], total: 0 }
|
||||||
this.albums = data.albums ? data.albums : { items: [], total: 0 }
|
this.albums = data.albums ? data.albums : { items: [], total: 0 }
|
||||||
this.playlists = data.playlists ? data.playlists : { items: [], total: 0 }
|
this.playlists = data.playlists ? data.playlists : { items: [], total: 0 }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
this.$store.commit(types.ADD_RECENT_SEARCH, route.query.query)
|
searchAudiobooks: function (query) {
|
||||||
|
if (query.type.indexOf('audiobook') < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchParams = {
|
||||||
|
type: 'album',
|
||||||
|
media_kind: 'audiobook'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.query.startsWith('query:')) {
|
||||||
|
searchParams.expression = query.query.replace(/^query:/, '').trim()
|
||||||
|
} else {
|
||||||
|
searchParams.expression = '((album includes "' + query.query + '" or artist includes "' + query.query + '") and media_kind is audiobook)'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.limit) {
|
||||||
|
searchParams.limit = query.limit
|
||||||
|
searchParams.offset = query.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
webapi.search(searchParams).then(({ data }) => {
|
||||||
|
this.audiobooks = data.albums ? data.albums : { items: [], total: 0 }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
searchPodcasts: function (query) {
|
||||||
|
if (query.type.indexOf('podcast') < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchParams = {
|
||||||
|
type: 'album',
|
||||||
|
media_kind: 'podcast'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.query.startsWith('query:')) {
|
||||||
|
searchParams.expression = query.query.replace(/^query:/, '').trim()
|
||||||
|
} else {
|
||||||
|
searchParams.expression = '((album includes "' + query.query + '" or artist includes "' + query.query + '") and media_kind is podcast)'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.limit) {
|
||||||
|
searchParams.limit = query.limit
|
||||||
|
searchParams.offset = query.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
webapi.search(searchParams).then(({ data }) => {
|
||||||
|
this.podcasts = data.albums ? data.albums : { items: [], total: 0 }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -247,7 +320,7 @@ export default {
|
|||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/search/library',
|
path: '/search/library',
|
||||||
query: {
|
query: {
|
||||||
type: 'track,artist,album,playlist',
|
type: 'track,artist,album,playlist,audiobook,podcast',
|
||||||
query: this.search_query,
|
query: this.search_query,
|
||||||
limit: 3,
|
limit: 3,
|
||||||
offset: 0
|
offset: 0
|
||||||
@ -296,45 +369,29 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
play_track: function (track) {
|
open_search_audiobooks: function () {
|
||||||
webapi.player_play_uri(track.uri, false)
|
this.$router.push({
|
||||||
|
path: '/search/library',
|
||||||
|
query: {
|
||||||
|
type: 'audiobook',
|
||||||
|
query: this.$route.query.query
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
open_artist: function (artist) {
|
open_search_podcasts: function () {
|
||||||
this.$router.push({ path: '/music/artists/' + artist.id })
|
this.$router.push({
|
||||||
},
|
path: '/search/library',
|
||||||
|
query: {
|
||||||
open_album: function (album) {
|
type: 'podcast',
|
||||||
this.$router.push({ path: '/music/albums/' + album.id })
|
query: this.$route.query.query
|
||||||
},
|
}
|
||||||
|
})
|
||||||
open_playlist: function (playlist) {
|
|
||||||
this.$router.push({ path: '/playlists/' + playlist.id + '/tracks' })
|
|
||||||
},
|
},
|
||||||
|
|
||||||
open_recent_search: function (query) {
|
open_recent_search: function (query) {
|
||||||
this.search_query = query
|
this.search_query = query
|
||||||
this.new_search()
|
this.new_search()
|
||||||
},
|
|
||||||
|
|
||||||
open_track_dialog: function (track) {
|
|
||||||
this.selected_track = track
|
|
||||||
this.show_track_details_modal = true
|
|
||||||
},
|
|
||||||
|
|
||||||
open_album_dialog: function (album) {
|
|
||||||
this.selected_album = album
|
|
||||||
this.show_album_details_modal = true
|
|
||||||
},
|
|
||||||
|
|
||||||
open_artist_dialog: function (artist) {
|
|
||||||
this.selected_artist = artist
|
|
||||||
this.show_artist_details_modal = true
|
|
||||||
},
|
|
||||||
|
|
||||||
open_playlist_dialog: function (playlist) {
|
|
||||||
this.selected_playlist = playlist
|
|
||||||
this.show_playlist_details_modal = true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -2,6 +2,54 @@
|
|||||||
<div>
|
<div>
|
||||||
<tabs-settings></tabs-settings>
|
<tabs-settings></tabs-settings>
|
||||||
|
|
||||||
|
<content-with-heading>
|
||||||
|
<template slot="heading-left">
|
||||||
|
<div class="title is-4">Navbar items</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template slot="content">
|
||||||
|
<p class="content">
|
||||||
|
Select the top navigation bar menu items
|
||||||
|
</p>
|
||||||
|
<div class="notification is-size-7">
|
||||||
|
Be aware that if you select more items than can be shown on your screen will result in the burger menu item to disapear.
|
||||||
|
</div>
|
||||||
|
<settings-checkbox category_name="webinterface" option_name="show_menu_item_playlists">
|
||||||
|
<template slot="label"> Playlists</template>
|
||||||
|
</settings-checkbox>
|
||||||
|
<settings-checkbox category_name="webinterface" option_name="show_menu_item_music">
|
||||||
|
<template slot="label"> Music</template>
|
||||||
|
</settings-checkbox>
|
||||||
|
<settings-checkbox category_name="webinterface" option_name="show_menu_item_podcasts">
|
||||||
|
<template slot="label"> Podcasts</template>
|
||||||
|
</settings-checkbox>
|
||||||
|
<settings-checkbox category_name="webinterface" option_name="show_menu_item_audiobooks">
|
||||||
|
<template slot="label"> Audiobooks</template>
|
||||||
|
</settings-checkbox>
|
||||||
|
<settings-checkbox category_name="webinterface" option_name="show_menu_item_radio">
|
||||||
|
<template slot="label"> Radio</template>
|
||||||
|
</settings-checkbox>
|
||||||
|
<settings-checkbox category_name="webinterface" option_name="show_menu_item_files">
|
||||||
|
<template slot="label"> Files</template>
|
||||||
|
</settings-checkbox>
|
||||||
|
<settings-checkbox category_name="webinterface" option_name="show_menu_item_search">
|
||||||
|
<template slot="label"> Search</template>
|
||||||
|
</settings-checkbox>
|
||||||
|
</template>
|
||||||
|
</content-with-heading>
|
||||||
|
|
||||||
|
<content-with-heading>
|
||||||
|
<template slot="heading-left">
|
||||||
|
<div class="title is-4">Album lists</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template slot="content">
|
||||||
|
<settings-checkbox category_name="webinterface" option_name="show_cover_artwork_in_album_lists">
|
||||||
|
<template slot="label"> Show cover artwork in album list</template>
|
||||||
|
</settings-checkbox>
|
||||||
|
</template>
|
||||||
|
</content-with-heading>
|
||||||
|
|
||||||
<content-with-heading>
|
<content-with-heading>
|
||||||
<template slot="heading-left">
|
<template slot="heading-left">
|
||||||
<div class="title is-4">Now playing page</div>
|
<div class="title is-4">Now playing page</div>
|
||||||
|
@ -15,7 +15,20 @@
|
|||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<p class="heading has-text-centered-mobile">{{ total }} albums</p>
|
<p class="heading has-text-centered-mobile">{{ total }} albums</p>
|
||||||
<spotify-list-item-album v-for="album in albums" :key="album.id" :album="album">
|
<spotify-list-item-album v-for="album in albums"
|
||||||
|
:key="album.id"
|
||||||
|
:album="album"
|
||||||
|
@click="open_album(album)">
|
||||||
|
<template slot="artwork" v-if="is_visible_artwork">
|
||||||
|
<p class="image is-64x64 fd-has-shadow fd-has-action">
|
||||||
|
<cover-artwork
|
||||||
|
:artwork_url="artwork_url(album)"
|
||||||
|
:artist="album.artist"
|
||||||
|
:album="album.name"
|
||||||
|
:maxwidth="64"
|
||||||
|
:maxheight="64" />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<template slot="actions">
|
<template slot="actions">
|
||||||
<a @click="open_dialog(album)">
|
<a @click="open_dialog(album)">
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||||
@ -35,6 +48,7 @@ import ContentWithHeading from '@/templates/ContentWithHeading'
|
|||||||
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
|
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
|
||||||
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
||||||
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist'
|
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist'
|
||||||
|
import CoverArtwork from '@/components/CoverArtwork'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
import SpotifyWebApi from 'spotify-web-api-js'
|
import SpotifyWebApi from 'spotify-web-api-js'
|
||||||
@ -63,7 +77,7 @@ const artistData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'SpotifyPageArtist',
|
name: 'SpotifyPageArtist',
|
||||||
mixins: [LoadDataBeforeEnterMixin(artistData)],
|
mixins: [LoadDataBeforeEnterMixin(artistData)],
|
||||||
components: { ContentWithHeading, SpotifyListItemAlbum, SpotifyModalDialogAlbum, SpotifyModalDialogArtist, InfiniteLoading },
|
components: { ContentWithHeading, SpotifyListItemAlbum, SpotifyModalDialogAlbum, SpotifyModalDialogArtist, InfiniteLoading, CoverArtwork },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -79,6 +93,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
is_visible_artwork () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_cover_artwork_in_album_lists').value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
load_next: function ($state) {
|
load_next: function ($state) {
|
||||||
const spotifyApi = new SpotifyWebApi()
|
const spotifyApi = new SpotifyWebApi()
|
||||||
@ -106,9 +126,20 @@ export default {
|
|||||||
webapi.player_play_uri(this.artist.uri, true)
|
webapi.player_play_uri(this.artist.uri, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
open_album: function (album) {
|
||||||
|
this.$router.push({ path: '/music/spotify/albums/' + album.id })
|
||||||
|
},
|
||||||
|
|
||||||
open_dialog: function (album) {
|
open_dialog: function (album) {
|
||||||
this.selected_album = album
|
this.selected_album = album
|
||||||
this.show_details_modal = true
|
this.show_details_modal = true
|
||||||
|
},
|
||||||
|
|
||||||
|
artwork_url: function (album) {
|
||||||
|
if (album.images && album.images.length > 0) {
|
||||||
|
return album.images[0].url
|
||||||
|
}
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,20 @@
|
|||||||
<p class="title is-4">New Releases</p>
|
<p class="title is-4">New Releases</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<spotify-list-item-album v-for="album in new_releases" :key="album.id" :album="album">
|
<spotify-list-item-album v-for="album in new_releases"
|
||||||
|
:key="album.id"
|
||||||
|
:album="album"
|
||||||
|
@click="open_album(album)">
|
||||||
|
<template slot="artwork" v-if="is_visible_artwork">
|
||||||
|
<p class="image is-64x64 fd-has-shadow fd-has-action">
|
||||||
|
<cover-artwork
|
||||||
|
:artwork_url="artwork_url(album)"
|
||||||
|
:artist="album.artist"
|
||||||
|
:album="album.name"
|
||||||
|
:maxwidth="64"
|
||||||
|
:maxheight="64" />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<template slot="actions">
|
<template slot="actions">
|
||||||
<a @click="open_album_dialog(album)">
|
<a @click="open_album_dialog(album)">
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||||
@ -64,6 +77,7 @@ import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
|
|||||||
import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist'
|
import SpotifyListItemPlaylist from '@/components/SpotifyListItemPlaylist'
|
||||||
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
||||||
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
|
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
|
||||||
|
import CoverArtwork from '@/components/CoverArtwork'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
import SpotifyWebApi from 'spotify-web-api-js'
|
import SpotifyWebApi from 'spotify-web-api-js'
|
||||||
@ -93,7 +107,7 @@ const browseData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'SpotifyPageBrowse',
|
name: 'SpotifyPageBrowse',
|
||||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||||
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist },
|
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist, CoverArtwork },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -112,10 +126,19 @@ export default {
|
|||||||
|
|
||||||
featured_playlists () {
|
featured_playlists () {
|
||||||
return this.$store.state.spotify_featured_playlists.slice(0, 3)
|
return this.$store.state.spotify_featured_playlists.slice(0, 3)
|
||||||
|
},
|
||||||
|
|
||||||
|
is_visible_artwork () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_cover_artwork_in_album_lists').value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
open_album: function (album) {
|
||||||
|
this.$router.push({ path: '/music/spotify/albums/' + album.id })
|
||||||
|
},
|
||||||
|
|
||||||
open_album_dialog: function (album) {
|
open_album_dialog: function (album) {
|
||||||
this.selected_album = album
|
this.selected_album = album
|
||||||
this.show_album_details_modal = true
|
this.show_album_details_modal = true
|
||||||
@ -124,6 +147,13 @@ export default {
|
|||||||
open_playlist_dialog: function (playlist) {
|
open_playlist_dialog: function (playlist) {
|
||||||
this.selected_playlist = playlist
|
this.selected_playlist = playlist
|
||||||
this.show_playlist_details_modal = true
|
this.show_playlist_details_modal = true
|
||||||
|
},
|
||||||
|
|
||||||
|
artwork_url: function (album) {
|
||||||
|
if (album.images && album.images.length > 0) {
|
||||||
|
return album.images[0].url
|
||||||
|
}
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,22 @@
|
|||||||
<p class="title is-4">New Releases</p>
|
<p class="title is-4">New Releases</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<spotify-list-item-album v-for="album in new_releases" :key="album.id" :album="album">
|
<spotify-list-item-album v-for="album in new_releases"
|
||||||
|
:key="album.id"
|
||||||
|
:album="album"
|
||||||
|
@click="open_album(album)">
|
||||||
|
<template slot="artwork" v-if="is_visible_artwork">
|
||||||
|
<p class="image is-64x64 fd-has-shadow fd-has-action">
|
||||||
|
<cover-artwork
|
||||||
|
:artwork_url="artwork_url(album)"
|
||||||
|
:artist="album.artist"
|
||||||
|
:album="album.name"
|
||||||
|
:maxwidth="64"
|
||||||
|
:maxheight="64" />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<template slot="actions">
|
<template slot="actions">
|
||||||
<a @click="open_album(album)">
|
<a @click="open_album_dialog(album)">
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
@ -26,6 +39,7 @@ import ContentWithHeading from '@/templates/ContentWithHeading'
|
|||||||
import TabsMusic from '@/components/TabsMusic'
|
import TabsMusic from '@/components/TabsMusic'
|
||||||
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
|
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
|
||||||
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
||||||
|
import CoverArtwork from '@/components/CoverArtwork'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
import SpotifyWebApi from 'spotify-web-api-js'
|
import SpotifyWebApi from 'spotify-web-api-js'
|
||||||
@ -51,7 +65,7 @@ const browseData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'SpotifyPageBrowseNewReleases',
|
name: 'SpotifyPageBrowseNewReleases',
|
||||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||||
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyModalDialogAlbum },
|
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyModalDialogAlbum, CoverArtwork },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -63,13 +77,29 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
new_releases () {
|
new_releases () {
|
||||||
return this.$store.state.spotify_new_releases
|
return this.$store.state.spotify_new_releases
|
||||||
|
},
|
||||||
|
|
||||||
|
is_visible_artwork () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_cover_artwork_in_album_lists').value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
open_album: function (album) {
|
open_album: function (album) {
|
||||||
|
this.$router.push({ path: '/music/spotify/albums/' + album.id })
|
||||||
|
},
|
||||||
|
|
||||||
|
open_album_dialog: function (album) {
|
||||||
this.selected_album = album
|
this.selected_album = album
|
||||||
this.show_album_details_modal = true
|
this.show_album_details_modal = true
|
||||||
|
},
|
||||||
|
|
||||||
|
artwork_url: function (album) {
|
||||||
|
if (album.images && album.images.length > 0) {
|
||||||
|
return album.images[0].url
|
||||||
|
}
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<nav v-if="show_all_tracks_button" class="level">
|
<nav v-if="show_all_tracks_button" class="level">
|
||||||
<p class="level-item">
|
<p class="level-item">
|
||||||
<a class="button is-light is-small is-rounded" v-on:click="open_search_tracks">Show all {{ tracks.total }} tracks</a>
|
<a class="button is-light is-small is-rounded" v-on:click="open_search_tracks">Show all {{ tracks.total.toLocaleString() }} tracks</a>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
<p v-if="!tracks.total">No results</p>
|
<p v-if="!tracks.total">No results</p>
|
||||||
@ -70,7 +70,7 @@
|
|||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<nav v-if="show_all_artists_button" class="level">
|
<nav v-if="show_all_artists_button" class="level">
|
||||||
<p class="level-item">
|
<p class="level-item">
|
||||||
<a class="button is-light is-small is-rounded" v-on:click="open_search_artists">Show all {{ artists.total }} artists</a>
|
<a class="button is-light is-small is-rounded" v-on:click="open_search_artists">Show all {{ artists.total.toLocaleString() }} artists</a>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
<p v-if="!artists.total">No results</p>
|
<p v-if="!artists.total">No results</p>
|
||||||
@ -83,7 +83,20 @@
|
|||||||
<p class="title is-4">Albums</p>
|
<p class="title is-4">Albums</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<spotify-list-item-album v-for="album in albums.items" :key="album.id" :album="album">
|
<spotify-list-item-album v-for="album in albums.items"
|
||||||
|
:key="album.id"
|
||||||
|
:album="album"
|
||||||
|
@click="open_album(album)">
|
||||||
|
<template slot="artwork" v-if="is_visible_artwork">
|
||||||
|
<p class="image is-64x64 fd-has-shadow fd-has-action">
|
||||||
|
<cover-artwork
|
||||||
|
:artwork_url="artwork_url(album)"
|
||||||
|
:artist="album.artist"
|
||||||
|
:album="album.name"
|
||||||
|
:maxwidth="64"
|
||||||
|
:maxheight="64" />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<template slot="actions">
|
<template slot="actions">
|
||||||
<a @click="open_album_dialog(album)">
|
<a @click="open_album_dialog(album)">
|
||||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||||
@ -96,7 +109,7 @@
|
|||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<nav v-if="show_all_albums_button" class="level">
|
<nav v-if="show_all_albums_button" class="level">
|
||||||
<p class="level-item">
|
<p class="level-item">
|
||||||
<a class="button is-light is-small is-rounded" v-on:click="open_search_albums">Show all {{ albums.total }} albums</a>
|
<a class="button is-light is-small is-rounded" v-on:click="open_search_albums">Show all {{ albums.total.toLocaleString() }} albums</a>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
<p v-if="!albums.total">No results</p>
|
<p v-if="!albums.total">No results</p>
|
||||||
@ -122,7 +135,7 @@
|
|||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<nav v-if="show_all_playlists_button" class="level">
|
<nav v-if="show_all_playlists_button" class="level">
|
||||||
<p class="level-item">
|
<p class="level-item">
|
||||||
<a class="button is-light is-small is-rounded" v-on:click="open_search_playlists">Show all {{ playlists.total }} playlists</a>
|
<a class="button is-light is-small is-rounded" v-on:click="open_search_playlists">Show all {{ playlists.total.toLocaleString() }} playlists</a>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
<p v-if="!playlists.total">No results</p>
|
<p v-if="!playlists.total">No results</p>
|
||||||
@ -142,6 +155,7 @@ import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack'
|
|||||||
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist'
|
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist'
|
||||||
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
||||||
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
|
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
|
||||||
|
import CoverArtwork from '@/components/CoverArtwork'
|
||||||
import SpotifyWebApi from 'spotify-web-api-js'
|
import SpotifyWebApi from 'spotify-web-api-js'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
@ -149,7 +163,7 @@ import InfiniteLoading from 'vue-infinite-loading'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SpotifyPageSearch',
|
name: 'SpotifyPageSearch',
|
||||||
components: { ContentWithHeading, TabsSearch, SpotifyListItemTrack, SpotifyListItemArtist, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogTrack, SpotifyModalDialogArtist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist, InfiniteLoading },
|
components: { ContentWithHeading, TabsSearch, SpotifyListItemTrack, SpotifyListItemArtist, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogTrack, SpotifyModalDialogArtist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist, InfiniteLoading, CoverArtwork },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -172,7 +186,9 @@ export default {
|
|||||||
selected_artist: {},
|
selected_artist: {},
|
||||||
|
|
||||||
show_playlist_details_modal: false,
|
show_playlist_details_modal: false,
|
||||||
selected_playlist: {}
|
selected_playlist: {},
|
||||||
|
|
||||||
|
validSearchTypes: ['track', 'artist', 'album', 'playlist']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -207,6 +223,10 @@ export default {
|
|||||||
},
|
},
|
||||||
show_all_playlists_button () {
|
show_all_playlists_button () {
|
||||||
return this.playlists.total > this.playlists.items.length
|
return this.playlists.total > this.playlists.items.length
|
||||||
|
},
|
||||||
|
|
||||||
|
is_visible_artwork () {
|
||||||
|
return this.$store.getters.settings_option('webinterface', 'show_cover_artwork_in_album_lists').value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -245,7 +265,8 @@ export default {
|
|||||||
var spotifyApi = new SpotifyWebApi()
|
var spotifyApi = new SpotifyWebApi()
|
||||||
spotifyApi.setAccessToken(data.webapi_token)
|
spotifyApi.setAccessToken(data.webapi_token)
|
||||||
|
|
||||||
return spotifyApi.search(this.query.query, this.query.type.split(','), this.search_param)
|
var types = this.query.type.split(',').filter(type => this.validSearchTypes.includes(type))
|
||||||
|
return spotifyApi.search(this.query.query, types, this.search_param)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -318,7 +339,7 @@ export default {
|
|||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/search/spotify',
|
path: '/search/spotify',
|
||||||
query: {
|
query: {
|
||||||
type: 'track,artist,album,playlist',
|
type: 'track,artist,album,playlist,audiobook,podcast',
|
||||||
query: this.search_query,
|
query: this.search_query,
|
||||||
limit: 3,
|
limit: 3,
|
||||||
offset: 0
|
offset: 0
|
||||||
@ -390,6 +411,17 @@ export default {
|
|||||||
open_playlist_dialog: function (playlist) {
|
open_playlist_dialog: function (playlist) {
|
||||||
this.selected_playlist = playlist
|
this.selected_playlist = playlist
|
||||||
this.show_playlist_details_modal = true
|
this.show_playlist_details_modal = true
|
||||||
|
},
|
||||||
|
|
||||||
|
open_album: function (album) {
|
||||||
|
this.$router.push({ path: '/music/spotify/albums/' + album.id })
|
||||||
|
},
|
||||||
|
|
||||||
|
artwork_url: function (album) {
|
||||||
|
if (album.images && album.images.length > 0) {
|
||||||
|
return album.images[0].url
|
||||||
|
}
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -17,8 +17,10 @@ import PageGenreTracks from '@/pages/PageGenreTracks'
|
|||||||
import PageArtistTracks from '@/pages/PageArtistTracks'
|
import PageArtistTracks from '@/pages/PageArtistTracks'
|
||||||
import PagePodcasts from '@/pages/PagePodcasts'
|
import PagePodcasts from '@/pages/PagePodcasts'
|
||||||
import PagePodcast from '@/pages/PagePodcast'
|
import PagePodcast from '@/pages/PagePodcast'
|
||||||
import PageAudiobooks from '@/pages/PageAudiobooks'
|
import PageAudiobooksAlbums from '@/pages/PageAudiobooksAlbums'
|
||||||
import PageAudiobook from '@/pages/PageAudiobook'
|
import PageAudiobooksArtists from '@/pages/PageAudiobooksArtists'
|
||||||
|
import PageAudiobooksArtist from '@/pages/PageAudiobooksArtist'
|
||||||
|
import PageAudiobooksAlbum from '@/pages/PageAudiobooksAlbum'
|
||||||
import PagePlaylists from '@/pages/PagePlaylists'
|
import PagePlaylists from '@/pages/PagePlaylists'
|
||||||
import PagePlaylist from '@/pages/PagePlaylist'
|
import PagePlaylist from '@/pages/PagePlaylist'
|
||||||
import PageFiles from '@/pages/PageFiles'
|
import PageFiles from '@/pages/PageFiles'
|
||||||
@ -126,12 +128,6 @@ export const router = new VueRouter({
|
|||||||
component: PageGenreTracks,
|
component: PageGenreTracks,
|
||||||
meta: { show_progress: true, has_index: true }
|
meta: { show_progress: true, has_index: true }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/music/radio',
|
|
||||||
name: 'Radio',
|
|
||||||
component: PageRadioStreams,
|
|
||||||
meta: { show_progress: true, has_tabs: true }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/podcasts',
|
path: '/podcasts',
|
||||||
name: 'Podcasts',
|
name: 'Podcasts',
|
||||||
@ -146,14 +142,36 @@ export const router = new VueRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/audiobooks',
|
path: '/audiobooks',
|
||||||
name: 'Audiobooks',
|
redirect: '/audiobooks/artists'
|
||||||
component: PageAudiobooks,
|
},
|
||||||
|
{
|
||||||
|
path: '/audiobooks/artists',
|
||||||
|
name: 'AudiobooksArtists',
|
||||||
|
component: PageAudiobooksArtists,
|
||||||
|
meta: { show_progress: true, has_tabs: true, has_index: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/audiobooks/artists/:artist_id',
|
||||||
|
name: 'AudiobooksArtist',
|
||||||
|
component: PageAudiobooksArtist,
|
||||||
meta: { show_progress: true }
|
meta: { show_progress: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/audiobooks/albums',
|
||||||
|
name: 'AudiobooksAlbums',
|
||||||
|
component: PageAudiobooksAlbums,
|
||||||
|
meta: { show_progress: true, has_tabs: true, has_index: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/audiobooks/:album_id',
|
path: '/audiobooks/:album_id',
|
||||||
name: 'Audiobook',
|
name: 'Audiobook',
|
||||||
component: PageAudiobook,
|
component: PageAudiobooksAlbum,
|
||||||
|
meta: { show_progress: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/radio',
|
||||||
|
name: 'Radio',
|
||||||
|
component: PageRadioStreams,
|
||||||
meta: { show_progress: true }
|
meta: { show_progress: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -258,11 +276,11 @@ export const router = new VueRouter({
|
|||||||
}, 10)
|
}, 10)
|
||||||
})
|
})
|
||||||
} else if (to.path === from.path && to.hash) {
|
} else if (to.path === from.path && to.hash) {
|
||||||
return { selector: to.hash, offset: { x: 0, y: 90 } }
|
return { selector: to.hash, offset: { x: 0, y: 120 } }
|
||||||
} else if (to.hash) {
|
} else if (to.hash) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve({ selector: to.hash, offset: { x: 0, y: 90 } })
|
resolve({ selector: to.hash, offset: { x: 0, y: 120 } })
|
||||||
}, 10)
|
}, 10)
|
||||||
})
|
})
|
||||||
} else if (to.meta.has_index) {
|
} else if (to.meta.has_index) {
|
||||||
|
@ -53,6 +53,9 @@ export default new Vuex.Store({
|
|||||||
recent_searches: [],
|
recent_searches: [],
|
||||||
|
|
||||||
hide_singles: false,
|
hide_singles: false,
|
||||||
|
hide_spotify: false,
|
||||||
|
artists_sort: 'Name',
|
||||||
|
albums_sort: 'Name',
|
||||||
show_only_next_items: false,
|
show_only_next_items: false,
|
||||||
show_burger_menu: false,
|
show_burger_menu: false,
|
||||||
show_player_menu: false
|
show_player_menu: false
|
||||||
@ -91,6 +94,18 @@ export default new Vuex.Store({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
settings_category: (state) => (categoryName) => {
|
||||||
|
return state.settings.categories.find(elem => elem.name === categoryName)
|
||||||
|
},
|
||||||
|
|
||||||
|
settings_option: (state) => (categoryName, optionName) => {
|
||||||
|
const category = state.settings.categories.find(elem => elem.name === categoryName)
|
||||||
|
if (!category) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return category.options.find(elem => elem.name === optionName)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -171,6 +186,15 @@ export default new Vuex.Store({
|
|||||||
[types.HIDE_SINGLES] (state, hideSingles) {
|
[types.HIDE_SINGLES] (state, hideSingles) {
|
||||||
state.hide_singles = hideSingles
|
state.hide_singles = hideSingles
|
||||||
},
|
},
|
||||||
|
[types.HIDE_SPOTIFY] (state, hideSpotify) {
|
||||||
|
state.hide_spotify = hideSpotify
|
||||||
|
},
|
||||||
|
[types.ARTISTS_SORT] (state, sort) {
|
||||||
|
state.artists_sort = sort
|
||||||
|
},
|
||||||
|
[types.ALBUMS_SORT] (state, sort) {
|
||||||
|
state.albums_sort = sort
|
||||||
|
},
|
||||||
[types.SHOW_ONLY_NEXT_ITEMS] (state, showOnlyNextItems) {
|
[types.SHOW_ONLY_NEXT_ITEMS] (state, showOnlyNextItems) {
|
||||||
state.show_only_next_items = showOnlyNextItems
|
state.show_only_next_items = showOnlyNextItems
|
||||||
},
|
},
|
||||||
|
@ -19,6 +19,9 @@ export const DELETE_NOTIFICATION = 'DELETE_NOTIFICATION'
|
|||||||
export const ADD_RECENT_SEARCH = 'ADD_RECENT_SEARCH'
|
export const ADD_RECENT_SEARCH = 'ADD_RECENT_SEARCH'
|
||||||
|
|
||||||
export const HIDE_SINGLES = 'HIDE_SINGLES'
|
export const HIDE_SINGLES = 'HIDE_SINGLES'
|
||||||
|
export const HIDE_SPOTIFY = 'HIDE_SPOTIFY'
|
||||||
|
export const ARTISTS_SORT = 'ARTISTS_SORT'
|
||||||
|
export const ALBUMS_SORT = 'ALBUMS_SORT'
|
||||||
export const SHOW_ONLY_NEXT_ITEMS = 'SHOW_ONLY_NEXT_ITEMS'
|
export const SHOW_ONLY_NEXT_ITEMS = 'SHOW_ONLY_NEXT_ITEMS'
|
||||||
export const SHOW_BURGER_MENU = 'SHOW_BURGER_MENU'
|
export const SHOW_BURGER_MENU = 'SHOW_BURGER_MENU'
|
||||||
export const SHOW_PLAYER_MENU = 'SHOW_PLAYER_MENU'
|
export const SHOW_PLAYER_MENU = 'SHOW_PLAYER_MENU'
|
||||||
|
@ -3,25 +3,35 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<div class="column is-four-fifths">
|
<div class="column is-four-fifths">
|
||||||
<slot name="options"></slot>
|
<section v-if="$slots['options']">
|
||||||
<nav class="level" id="top">
|
<div v-observe-visibility="observer_options"></div>
|
||||||
<!-- Left side -->
|
<slot name="options"></slot>
|
||||||
<div class="level-left">
|
<nav class="buttons is-centered" style="margin-bottom: 6px; margin-top: 16px;">
|
||||||
<div class="level-item has-text-centered-mobile">
|
<a v-if="!options_visible" class="button is-small is-white" @click="scroll_to_top"><span class="icon is-small"><i class="mdi mdi-chevron-up"></i></span></a>
|
||||||
<div>
|
<a v-else class="button is-small is-white" @click="scroll_to_content"><span class="icon is-small"><i class="mdi mdi-chevron-down"></i></span></a>
|
||||||
<slot name="heading-left"></slot>
|
</nav>
|
||||||
|
</section>
|
||||||
|
<div :class="{'fd-content-with-option': $slots['options']}">
|
||||||
|
<nav class="level" id="top">
|
||||||
|
<!-- Left side -->
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item has-text-centered-mobile">
|
||||||
|
<div>
|
||||||
|
<slot name="heading-left"></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right side -->
|
<!-- Right side -->
|
||||||
<div class="level-right has-text-centered-mobile">
|
<div class="level-right has-text-centered-mobile">
|
||||||
<slot name="heading-right"></slot>
|
<slot name="heading-right"></slot>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<slot name="content"></slot>
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<slot name="footer"></slot>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
<slot name="content"></slot>
|
|
||||||
<div style="margin-top: 16px;">
|
|
||||||
<slot name="footer"></slot>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -30,6 +40,41 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ContentWithHeading',
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
options_visible: false,
|
||||||
|
observer_options: {
|
||||||
|
callback: this.visibilityChanged,
|
||||||
|
intersection: {
|
||||||
|
rootMargin: '-100px',
|
||||||
|
threshold: 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
scroll_to_top: function () {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
},
|
||||||
|
|
||||||
|
scroll_to_content: function () {
|
||||||
|
// window.scrollTo({ top: 80, behavior: 'smooth' })
|
||||||
|
if (this.$route.meta.has_tabs) {
|
||||||
|
this.$scrollTo('#top', { offset: -140 })
|
||||||
|
} else {
|
||||||
|
this.$scrollTo('#top', { offset: -100 })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
visibilityChanged: function (isVisible) {
|
||||||
|
this.options_visible = isVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -12,51 +12,51 @@ axios.interceptors.response.use(function (response) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
config () {
|
config () {
|
||||||
return axios.get('/api/config')
|
return axios.get('./api/config')
|
||||||
},
|
},
|
||||||
|
|
||||||
settings () {
|
settings () {
|
||||||
return axios.get('/api/settings')
|
return axios.get('./api/settings')
|
||||||
},
|
},
|
||||||
|
|
||||||
settings_update (categoryName, option) {
|
settings_update (categoryName, option) {
|
||||||
return axios.put('/api/settings/' + categoryName + '/' + option.name, option)
|
return axios.put('./api/settings/' + categoryName + '/' + option.name, option)
|
||||||
},
|
},
|
||||||
|
|
||||||
library_stats () {
|
library_stats () {
|
||||||
return axios.get('/api/library')
|
return axios.get('./api/library')
|
||||||
},
|
},
|
||||||
|
|
||||||
library_update () {
|
library_update () {
|
||||||
return axios.put('/api/update')
|
return axios.put('./api/update')
|
||||||
},
|
},
|
||||||
|
|
||||||
library_rescan () {
|
library_rescan () {
|
||||||
return axios.put('/api/rescan')
|
return axios.put('./api/rescan')
|
||||||
},
|
},
|
||||||
|
|
||||||
library_count (expression) {
|
library_count (expression) {
|
||||||
return axios.get('/api/library/count?expression=' + expression)
|
return axios.get('./api/library/count?expression=' + expression)
|
||||||
},
|
},
|
||||||
|
|
||||||
queue () {
|
queue () {
|
||||||
return axios.get('/api/queue')
|
return axios.get('./api/queue')
|
||||||
},
|
},
|
||||||
|
|
||||||
queue_clear () {
|
queue_clear () {
|
||||||
return axios.put('/api/queue/clear')
|
return axios.put('./api/queue/clear')
|
||||||
},
|
},
|
||||||
|
|
||||||
queue_remove (itemId) {
|
queue_remove (itemId) {
|
||||||
return axios.delete('/api/queue/items/' + itemId)
|
return axios.delete('./api/queue/items/' + itemId)
|
||||||
},
|
},
|
||||||
|
|
||||||
queue_move (itemId, newPosition) {
|
queue_move (itemId, newPosition) {
|
||||||
return axios.put('/api/queue/items/' + itemId + '?new_position=' + newPosition)
|
return axios.put('./api/queue/items/' + itemId + '?new_position=' + newPosition)
|
||||||
},
|
},
|
||||||
|
|
||||||
queue_add (uri) {
|
queue_add (uri) {
|
||||||
return axios.post('/api/queue/items/add?uris=' + uri).then((response) => {
|
return axios.post('./api/queue/items/add?uris=' + uri).then((response) => {
|
||||||
store.dispatch('add_notification', { text: response.data.count + ' tracks appended to queue', type: 'info', timeout: 2000 })
|
store.dispatch('add_notification', { text: response.data.count + ' tracks appended to queue', type: 'info', timeout: 2000 })
|
||||||
return Promise.resolve(response)
|
return Promise.resolve(response)
|
||||||
})
|
})
|
||||||
@ -67,7 +67,7 @@ export default {
|
|||||||
if (store.getters.now_playing && store.getters.now_playing.id) {
|
if (store.getters.now_playing && store.getters.now_playing.id) {
|
||||||
position = store.getters.now_playing.position + 1
|
position = store.getters.now_playing.position + 1
|
||||||
}
|
}
|
||||||
return axios.post('/api/queue/items/add?uris=' + uri + '&position=' + position).then((response) => {
|
return axios.post('./api/queue/items/add?uris=' + uri + '&position=' + position).then((response) => {
|
||||||
store.dispatch('add_notification', { text: response.data.count + ' tracks appended to queue', type: 'info', timeout: 2000 })
|
store.dispatch('add_notification', { text: response.data.count + ' tracks appended to queue', type: 'info', timeout: 2000 })
|
||||||
return Promise.resolve(response)
|
return Promise.resolve(response)
|
||||||
})
|
})
|
||||||
@ -77,7 +77,7 @@ export default {
|
|||||||
var options = {}
|
var options = {}
|
||||||
options.expression = expression
|
options.expression = expression
|
||||||
|
|
||||||
return axios.post('/api/queue/items/add', undefined, { params: options }).then((response) => {
|
return axios.post('./api/queue/items/add', undefined, { params: options }).then((response) => {
|
||||||
store.dispatch('add_notification', { text: response.data.count + ' tracks appended to queue', type: 'info', timeout: 2000 })
|
store.dispatch('add_notification', { text: response.data.count + ' tracks appended to queue', type: 'info', timeout: 2000 })
|
||||||
return Promise.resolve(response)
|
return Promise.resolve(response)
|
||||||
})
|
})
|
||||||
@ -91,21 +91,21 @@ export default {
|
|||||||
options.position = store.getters.now_playing.position + 1
|
options.position = store.getters.now_playing.position + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return axios.post('/api/queue/items/add', undefined, { params: options }).then((response) => {
|
return axios.post('./api/queue/items/add', undefined, { params: options }).then((response) => {
|
||||||
store.dispatch('add_notification', { text: response.data.count + ' tracks appended to queue', type: 'info', timeout: 2000 })
|
store.dispatch('add_notification', { text: response.data.count + ' tracks appended to queue', type: 'info', timeout: 2000 })
|
||||||
return Promise.resolve(response)
|
return Promise.resolve(response)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
queue_save_playlist (name) {
|
queue_save_playlist (name) {
|
||||||
return axios.post('/api/queue/save', undefined, { params: { name: name } }).then((response) => {
|
return axios.post('./api/queue/save', undefined, { params: { name: name } }).then((response) => {
|
||||||
store.dispatch('add_notification', { text: 'Queue saved to playlist "' + name + '"', type: 'info', timeout: 2000 })
|
store.dispatch('add_notification', { text: 'Queue saved to playlist "' + name + '"', type: 'info', timeout: 2000 })
|
||||||
return Promise.resolve(response)
|
return Promise.resolve(response)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
player_status () {
|
player_status () {
|
||||||
return axios.get('/api/player')
|
return axios.get('./api/player')
|
||||||
},
|
},
|
||||||
|
|
||||||
player_play_uri (uris, shuffle, position = undefined) {
|
player_play_uri (uris, shuffle, position = undefined) {
|
||||||
@ -116,7 +116,7 @@ export default {
|
|||||||
options.playback = 'start'
|
options.playback = 'start'
|
||||||
options.playback_from_position = position
|
options.playback_from_position = position
|
||||||
|
|
||||||
return axios.post('/api/queue/items/add', undefined, { params: options })
|
return axios.post('./api/queue/items/add', undefined, { params: options })
|
||||||
},
|
},
|
||||||
|
|
||||||
player_play_expression (expression, shuffle, position = undefined) {
|
player_play_expression (expression, shuffle, position = undefined) {
|
||||||
@ -127,111 +127,111 @@ export default {
|
|||||||
options.playback = 'start'
|
options.playback = 'start'
|
||||||
options.playback_from_position = position
|
options.playback_from_position = position
|
||||||
|
|
||||||
return axios.post('/api/queue/items/add', undefined, { params: options })
|
return axios.post('./api/queue/items/add', undefined, { params: options })
|
||||||
},
|
},
|
||||||
|
|
||||||
player_play (options = {}) {
|
player_play (options = {}) {
|
||||||
return axios.put('/api/player/play', undefined, { params: options })
|
return axios.put('./api/player/play', undefined, { params: options })
|
||||||
},
|
},
|
||||||
|
|
||||||
player_playpos (position) {
|
player_playpos (position) {
|
||||||
return axios.put('/api/player/play?position=' + position)
|
return axios.put('./api/player/play?position=' + position)
|
||||||
},
|
},
|
||||||
|
|
||||||
player_playid (itemId) {
|
player_playid (itemId) {
|
||||||
return axios.put('/api/player/play?item_id=' + itemId)
|
return axios.put('./api/player/play?item_id=' + itemId)
|
||||||
},
|
},
|
||||||
|
|
||||||
player_pause () {
|
player_pause () {
|
||||||
return axios.put('/api/player/pause')
|
return axios.put('./api/player/pause')
|
||||||
},
|
},
|
||||||
|
|
||||||
player_stop () {
|
player_stop () {
|
||||||
return axios.put('/api/player/stop')
|
return axios.put('./api/player/stop')
|
||||||
},
|
},
|
||||||
|
|
||||||
player_next () {
|
player_next () {
|
||||||
return axios.put('/api/player/next')
|
return axios.put('./api/player/next')
|
||||||
},
|
},
|
||||||
|
|
||||||
player_previous () {
|
player_previous () {
|
||||||
return axios.put('/api/player/previous')
|
return axios.put('./api/player/previous')
|
||||||
},
|
},
|
||||||
|
|
||||||
player_shuffle (newState) {
|
player_shuffle (newState) {
|
||||||
var shuffle = newState ? 'true' : 'false'
|
var shuffle = newState ? 'true' : 'false'
|
||||||
return axios.put('/api/player/shuffle?state=' + shuffle)
|
return axios.put('./api/player/shuffle?state=' + shuffle)
|
||||||
},
|
},
|
||||||
|
|
||||||
player_consume (newState) {
|
player_consume (newState) {
|
||||||
var consume = newState ? 'true' : 'false'
|
var consume = newState ? 'true' : 'false'
|
||||||
return axios.put('/api/player/consume?state=' + consume)
|
return axios.put('./api/player/consume?state=' + consume)
|
||||||
},
|
},
|
||||||
|
|
||||||
player_repeat (newRepeatMode) {
|
player_repeat (newRepeatMode) {
|
||||||
return axios.put('/api/player/repeat?state=' + newRepeatMode)
|
return axios.put('./api/player/repeat?state=' + newRepeatMode)
|
||||||
},
|
},
|
||||||
|
|
||||||
player_volume (volume) {
|
player_volume (volume) {
|
||||||
return axios.put('/api/player/volume?volume=' + volume)
|
return axios.put('./api/player/volume?volume=' + volume)
|
||||||
},
|
},
|
||||||
|
|
||||||
player_output_volume (outputId, outputVolume) {
|
player_output_volume (outputId, outputVolume) {
|
||||||
return axios.put('/api/player/volume?volume=' + outputVolume + '&output_id=' + outputId)
|
return axios.put('./api/player/volume?volume=' + outputVolume + '&output_id=' + outputId)
|
||||||
},
|
},
|
||||||
|
|
||||||
player_seek_to_pos (newPosition) {
|
player_seek_to_pos (newPosition) {
|
||||||
return axios.put('/api/player/seek?position_ms=' + newPosition)
|
return axios.put('./api/player/seek?position_ms=' + newPosition)
|
||||||
},
|
},
|
||||||
|
|
||||||
player_seek (seekMs) {
|
player_seek (seekMs) {
|
||||||
return axios.put('/api/player/seek?seek_ms=' + seekMs)
|
return axios.put('./api/player/seek?seek_ms=' + seekMs)
|
||||||
},
|
},
|
||||||
|
|
||||||
outputs () {
|
outputs () {
|
||||||
return axios.get('/api/outputs')
|
return axios.get('./api/outputs')
|
||||||
},
|
},
|
||||||
|
|
||||||
output_update (outputId, output) {
|
output_update (outputId, output) {
|
||||||
return axios.put('/api/outputs/' + outputId, output)
|
return axios.put('./api/outputs/' + outputId, output)
|
||||||
},
|
},
|
||||||
|
|
||||||
output_toggle (outputId) {
|
output_toggle (outputId) {
|
||||||
return axios.put('/api/outputs/' + outputId + '/toggle')
|
return axios.put('./api/outputs/' + outputId + '/toggle')
|
||||||
},
|
},
|
||||||
|
|
||||||
library_artists () {
|
library_artists (media_kind = undefined) {
|
||||||
return axios.get('/api/library/artists?media_kind=music')
|
return axios.get('./api/library/artists', { params: { media_kind: media_kind } })
|
||||||
},
|
},
|
||||||
|
|
||||||
library_artist (artistId) {
|
library_artist (artistId) {
|
||||||
return axios.get('/api/library/artists/' + artistId)
|
return axios.get('./api/library/artists/' + artistId)
|
||||||
},
|
},
|
||||||
|
|
||||||
library_artist_albums (artistId) {
|
library_artist_albums (artistId) {
|
||||||
return axios.get('/api/library/artists/' + artistId + '/albums')
|
return axios.get('./api/library/artists/' + artistId + '/albums')
|
||||||
},
|
},
|
||||||
|
|
||||||
library_albums (media_kind = undefined) {
|
library_albums (media_kind = undefined) {
|
||||||
return axios.get('/api/library/albums', { params: { media_kind: media_kind } })
|
return axios.get('./api/library/albums', { params: { media_kind: media_kind } })
|
||||||
},
|
},
|
||||||
|
|
||||||
library_album (albumId) {
|
library_album (albumId) {
|
||||||
return axios.get('/api/library/albums/' + albumId)
|
return axios.get('./api/library/albums/' + albumId)
|
||||||
},
|
},
|
||||||
|
|
||||||
library_album_tracks (albumId, filter = { limit: -1, offset: 0 }) {
|
library_album_tracks (albumId, filter = { limit: -1, offset: 0 }) {
|
||||||
return axios.get('/api/library/albums/' + albumId + '/tracks', {
|
return axios.get('./api/library/albums/' + albumId + '/tracks', {
|
||||||
params: filter
|
params: filter
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
library_album_track_update (albumId, attributes) {
|
library_album_track_update (albumId, attributes) {
|
||||||
return axios.put('/api/library/albums/' + albumId + '/tracks', undefined, { params: attributes })
|
return axios.put('./api/library/albums/' + albumId + '/tracks', undefined, { params: attributes })
|
||||||
},
|
},
|
||||||
|
|
||||||
library_genres () {
|
library_genres () {
|
||||||
return axios.get('/api/library/genres')
|
return axios.get('./api/library/genres')
|
||||||
},
|
},
|
||||||
|
|
||||||
library_genre (genre) {
|
library_genre (genre) {
|
||||||
@ -240,7 +240,7 @@ export default {
|
|||||||
media_kind: 'music',
|
media_kind: 'music',
|
||||||
expression: 'genre is "' + genre + '"'
|
expression: 'genre is "' + genre + '"'
|
||||||
}
|
}
|
||||||
return axios.get('/api/search', {
|
return axios.get('./api/search', {
|
||||||
params: genreParams
|
params: genreParams
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -251,7 +251,7 @@ export default {
|
|||||||
media_kind: 'music',
|
media_kind: 'music',
|
||||||
expression: 'genre is "' + genre + '"'
|
expression: 'genre is "' + genre + '"'
|
||||||
}
|
}
|
||||||
return axios.get('/api/search', {
|
return axios.get('./api/search', {
|
||||||
params: genreParams
|
params: genreParams
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -262,7 +262,7 @@ export default {
|
|||||||
media_kind: 'music',
|
media_kind: 'music',
|
||||||
expression: 'data_kind is url and song_length = 0'
|
expression: 'data_kind is url and song_length = 0'
|
||||||
}
|
}
|
||||||
return axios.get('/api/search', {
|
return axios.get('./api/search', {
|
||||||
params: params
|
params: params
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -273,7 +273,7 @@ export default {
|
|||||||
type: 'tracks',
|
type: 'tracks',
|
||||||
expression: 'songartistid is "' + artist + '"'
|
expression: 'songartistid is "' + artist + '"'
|
||||||
}
|
}
|
||||||
return axios.get('/api/search', {
|
return axios.get('./api/search', {
|
||||||
params: artistParams
|
params: artistParams
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -284,7 +284,7 @@ export default {
|
|||||||
type: 'tracks',
|
type: 'tracks',
|
||||||
expression: 'media_kind is podcast and play_count = 0 ORDER BY time_added DESC'
|
expression: 'media_kind is podcast and play_count = 0 ORDER BY time_added DESC'
|
||||||
}
|
}
|
||||||
return axios.get('/api/search', {
|
return axios.get('./api/search', {
|
||||||
params: episodesParams
|
params: episodesParams
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -294,86 +294,86 @@ export default {
|
|||||||
type: 'tracks',
|
type: 'tracks',
|
||||||
expression: 'media_kind is podcast and songalbumid is "' + albumId + '" ORDER BY date_released DESC'
|
expression: 'media_kind is podcast and songalbumid is "' + albumId + '" ORDER BY date_released DESC'
|
||||||
}
|
}
|
||||||
return axios.get('/api/search', {
|
return axios.get('./api/search', {
|
||||||
params: episodesParams
|
params: episodesParams
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
library_add (url) {
|
library_add (url) {
|
||||||
return axios.post('/api/library/add', undefined, { params: { url: url } })
|
return axios.post('./api/library/add', undefined, { params: { url: url } })
|
||||||
},
|
},
|
||||||
|
|
||||||
library_playlist_delete (playlistId) {
|
library_playlist_delete (playlistId) {
|
||||||
return axios.delete('/api/library/playlists/' + playlistId, undefined)
|
return axios.delete('./api/library/playlists/' + playlistId, undefined)
|
||||||
},
|
},
|
||||||
|
|
||||||
library_playlists () {
|
library_playlists () {
|
||||||
return axios.get('/api/library/playlists')
|
return axios.get('./api/library/playlists')
|
||||||
},
|
},
|
||||||
|
|
||||||
library_playlist_folder (playlistId = 0) {
|
library_playlist_folder (playlistId = 0) {
|
||||||
return axios.get('/api/library/playlists/' + playlistId + '/playlists')
|
return axios.get('./api/library/playlists/' + playlistId + '/playlists')
|
||||||
},
|
},
|
||||||
|
|
||||||
library_playlist (playlistId) {
|
library_playlist (playlistId) {
|
||||||
return axios.get('/api/library/playlists/' + playlistId)
|
return axios.get('./api/library/playlists/' + playlistId)
|
||||||
},
|
},
|
||||||
|
|
||||||
library_playlist_tracks (playlistId) {
|
library_playlist_tracks (playlistId) {
|
||||||
return axios.get('/api/library/playlists/' + playlistId + '/tracks')
|
return axios.get('./api/library/playlists/' + playlistId + '/tracks')
|
||||||
},
|
},
|
||||||
|
|
||||||
library_track (trackId) {
|
library_track (trackId) {
|
||||||
return axios.get('/api/library/tracks/' + trackId)
|
return axios.get('./api/library/tracks/' + trackId)
|
||||||
},
|
},
|
||||||
|
|
||||||
library_track_playlists (trackId) {
|
library_track_playlists (trackId) {
|
||||||
return axios.get('/api/library/tracks/' + trackId + '/playlists')
|
return axios.get('./api/library/tracks/' + trackId + '/playlists')
|
||||||
},
|
},
|
||||||
|
|
||||||
library_track_update (trackId, attributes = {}) {
|
library_track_update (trackId, attributes = {}) {
|
||||||
return axios.put('/api/library/tracks/' + trackId, undefined, { params: attributes })
|
return axios.put('./api/library/tracks/' + trackId, undefined, { params: attributes })
|
||||||
},
|
},
|
||||||
|
|
||||||
library_files (directory = undefined) {
|
library_files (directory = undefined) {
|
||||||
var filesParams = { directory: directory }
|
var filesParams = { directory: directory }
|
||||||
return axios.get('/api/library/files', {
|
return axios.get('./api/library/files', {
|
||||||
params: filesParams
|
params: filesParams
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
search (searchParams) {
|
search (searchParams) {
|
||||||
return axios.get('/api/search', {
|
return axios.get('./api/search', {
|
||||||
params: searchParams
|
params: searchParams
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
spotify () {
|
spotify () {
|
||||||
return axios.get('/api/spotify')
|
return axios.get('./api/spotify')
|
||||||
},
|
},
|
||||||
|
|
||||||
spotify_login (credentials) {
|
spotify_login (credentials) {
|
||||||
return axios.post('/api/spotify-login', credentials)
|
return axios.post('./api/spotify-login', credentials)
|
||||||
},
|
},
|
||||||
|
|
||||||
lastfm () {
|
lastfm () {
|
||||||
return axios.get('/api/lastfm')
|
return axios.get('./api/lastfm')
|
||||||
},
|
},
|
||||||
|
|
||||||
lastfm_login (credentials) {
|
lastfm_login (credentials) {
|
||||||
return axios.post('/api/lastfm-login', credentials)
|
return axios.post('./api/lastfm-login', credentials)
|
||||||
},
|
},
|
||||||
|
|
||||||
lastfm_logout (credentials) {
|
lastfm_logout (credentials) {
|
||||||
return axios.get('/api/lastfm-logout')
|
return axios.get('./api/lastfm-logout')
|
||||||
},
|
},
|
||||||
|
|
||||||
pairing () {
|
pairing () {
|
||||||
return axios.get('/api/pairing')
|
return axios.get('./api/pairing')
|
||||||
},
|
},
|
||||||
|
|
||||||
pairing_kickoff (pairingReq) {
|
pairing_kickoff (pairingReq) {
|
||||||
return axios.post('/api/pairing', pairingReq)
|
return axios.post('./api/pairing', pairingReq)
|
||||||
},
|
},
|
||||||
|
|
||||||
artwork_url_append_size_params (artworkUrl, maxwidth = 600, maxheight = 600) {
|
artwork_url_append_size_params (artworkUrl, maxwidth = 600, maxheight = 600) {
|
||||||
|
@ -10,6 +10,9 @@ module.exports = {
|
|||||||
|
|
||||||
assetsDir: 'player',
|
assetsDir: 'player',
|
||||||
|
|
||||||
|
// Relative public path
|
||||||
|
publicPath: './',
|
||||||
|
|
||||||
// Do not add hashes to the generated js/css filenames, would otherwise
|
// Do not add hashes to the generated js/css filenames, would otherwise
|
||||||
// require to adjust the Makefile in htdocs each time the web interface is
|
// require to adjust the Makefile in htdocs each time the web interface is
|
||||||
// build
|
// build
|
||||||
|
Loading…
x
Reference in New Issue
Block a user