mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-14 08:15:02 -05: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"> -->
|
||||
|
||||
<!-- Local libraries -->
|
||||
<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/fontawesome/css/all.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>
|
||||
|
||||
<body>
|
||||
@ -25,7 +25,7 @@
|
||||
-->
|
||||
<nav class="navbar">
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
@ -281,9 +281,9 @@
|
||||
<!-- <script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script> -->
|
||||
|
||||
<!-- Local libraries -->
|
||||
<script src="/admin/vendor/vue/vue.min.js"></script>
|
||||
<script src="/admin/vendor/axios/axios.min.js"></script>
|
||||
<script src="/admin/js/forked-daapd.js"></script>
|
||||
<script src="admin/vendor/vue/vue.min.js"></script>
|
||||
<script src="admin/vendor/axios/axios.min.js"></script>
|
||||
<script src="admin/js/forked-daapd.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -35,43 +35,43 @@ var app = new Vue({
|
||||
|
||||
methods: {
|
||||
loadConfig: function() {
|
||||
axios.get('/api/config').then(response => {
|
||||
axios.get('./api/config').then(response => {
|
||||
this.config = response.data;
|
||||
this.connect()});
|
||||
},
|
||||
|
||||
loadLibrary: function() {
|
||||
axios.get('/api/library').then(response => this.library = response.data);
|
||||
axios.get('./api/library').then(response => this.library = response.data);
|
||||
},
|
||||
|
||||
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() {
|
||||
axios.get('/api/spotify').then(response => this.spotify = response.data);
|
||||
axios.get('./api/spotify').then(response => this.spotify = response.data);
|
||||
},
|
||||
|
||||
loadPairing: function() {
|
||||
axios.get('/api/pairing').then(response => this.pairing = response.data);
|
||||
axios.get('./api/pairing').then(response => this.pairing = response.data);
|
||||
},
|
||||
|
||||
loadLastfm: function() {
|
||||
axios.get('/api/lastfm').then(response => this.lastfm = response.data);
|
||||
axios.get('./api/lastfm').then(response => this.lastfm = response.data);
|
||||
},
|
||||
|
||||
update: function() {
|
||||
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() {
|
||||
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() {
|
||||
axios.post('/api/pairing', this.pairing_req).then(response => {
|
||||
axios.post('./api/pairing', this.pairing_req).then(response => {
|
||||
console.log('Kicked off pairing');
|
||||
if (!this.config.websocket_port) {
|
||||
this.pairing = {};
|
||||
@ -80,7 +80,7 @@ var app = new Vue({
|
||||
},
|
||||
|
||||
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');
|
||||
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) {
|
||||
this.loadOutputs();
|
||||
}
|
||||
@ -102,7 +102,7 @@ var app = new Vue({
|
||||
},
|
||||
|
||||
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.password = '';
|
||||
this.libspotify.errors.user = '';
|
||||
@ -117,7 +117,7 @@ var app = new Vue({
|
||||
},
|
||||
|
||||
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.password = '';
|
||||
this.lastfm_login.errors.user = '';
|
||||
@ -132,7 +132,7 @@ var app = new Vue({
|
||||
},
|
||||
|
||||
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) {
|
||||
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
|
||||
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);
|
||||
|
||||
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
|
||||
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);
|
||||
|
||||
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
|
||||
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
|
||||
httpd_request_etag_matches(struct evhttp_request *req, const char *etag);
|
||||
|
||||
void
|
||||
httpd_response_not_cachable(struct evhttp_request *req);
|
||||
|
||||
/*
|
||||
* Gzips an evbuffer
|
||||
*
|
||||
|
@ -213,7 +213,7 @@ artist_to_json(struct db_group_info *dbgri)
|
||||
if (ret < sizeof(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))
|
||||
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))
|
||||
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))
|
||||
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;
|
||||
char uri[100];
|
||||
int intval;
|
||||
bool boolval;
|
||||
int ret;
|
||||
|
||||
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));
|
||||
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));
|
||||
}
|
||||
|
||||
@ -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_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, "artwork_url", json_object_new_string("/artwork/nowplaying"));
|
||||
json_object_object_add(reply, "artwork_url", json_object_new_string("./artwork/nowplaying"));
|
||||
}
|
||||
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
|
||||
// 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))
|
||||
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
|
||||
// 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).
|
||||
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))
|
||||
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 ret = 0;
|
||||
|
||||
if (!is_modified(hreq->req, DB_ADMIN_DB_MODIFIED))
|
||||
return HTTP_NOTMODIFIED;
|
||||
// Due to smart playlists possibly changing their tracks between rescans, disable caching in clients
|
||||
httpd_response_not_cachable(hreq->req);
|
||||
|
||||
ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &playlist_id);
|
||||
if (ret < 0)
|
||||
|
@ -24,22 +24,29 @@
|
||||
#include "db.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[] =
|
||||
{
|
||||
{ "show_composer_now_playing", SETTINGS_TYPE_BOOL },
|
||||
{ "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[] =
|
||||
{
|
||||
{ "use_artwork_source_spotify", SETTINGS_TYPE_BOOL, NULL, artwork_spotify_default_getbool, NULL },
|
||||
{ "use_artwork_source_discogs", SETTINGS_TYPE_BOOL, NULL, artwork_discogs_default_getbool, NULL },
|
||||
{ "use_artwork_source_coverartarchive", SETTINGS_TYPE_BOOL, NULL, artwork_coverartarchive_default_getbool, NULL },
|
||||
// Spotify source 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.
|
||||
{ "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[] =
|
||||
@ -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 -----------------------------*/
|
||||
|
||||
int
|
||||
@ -185,8 +147,8 @@ settings_option_getint(struct settings_option *option)
|
||||
if (ret == 0)
|
||||
return intval;
|
||||
|
||||
if (option->default_getint)
|
||||
return option->default_getint(option);
|
||||
if (option->default_value.intval)
|
||||
return option->default_value.intval;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -204,8 +166,8 @@ settings_option_getbool(struct settings_option *option)
|
||||
if (ret == 0)
|
||||
return (intval != 0);
|
||||
|
||||
if (option->default_getbool)
|
||||
return option->default_getbool(option);
|
||||
if (option->default_value.boolval)
|
||||
return option->default_value.boolval;
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -223,8 +185,8 @@ settings_option_getstr(struct settings_option *option)
|
||||
if (ret == 0)
|
||||
return s;
|
||||
|
||||
if (option->default_getstr)
|
||||
return option->default_getstr(option);
|
||||
if (option->default_value.strval)
|
||||
return option->default_value.strval;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
@ -12,12 +12,16 @@ enum settings_type {
|
||||
SETTINGS_TYPE_CATEGORY,
|
||||
};
|
||||
|
||||
union settings_default_value {
|
||||
int intval;
|
||||
bool boolval;
|
||||
char *strval;
|
||||
};
|
||||
|
||||
struct settings_option {
|
||||
const char *name;
|
||||
enum settings_type type;
|
||||
int (*default_getint)(struct settings_option *option);
|
||||
bool (*default_getbool)(struct settings_option *option);
|
||||
char *(*default_getstr)(struct settings_option *option);
|
||||
union settings_default_value default_value;
|
||||
};
|
||||
|
||||
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",
|
||||
"version": "0.7.2",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"description": "forked-daapd web interface",
|
||||
"author": "chme <christian.meffert@googlemail.com>",
|
||||
@ -11,41 +11,44 @@
|
||||
"dev": "vue-cli-service serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.2",
|
||||
"axios": "^0.20.0",
|
||||
"bulma": "^0.9.0",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"core-js": "^3.6.5",
|
||||
"mdi": "^2.2.43",
|
||||
"moment": "^2.27.0",
|
||||
"moment": "^2.28.0",
|
||||
"moment-duration-format": "^2.3.2",
|
||||
"npm": "^6.14.5",
|
||||
"npm": "^6.14.8",
|
||||
"reconnectingwebsocket": "^1.0.0",
|
||||
"spotify-web-api-js": "^1.4.0",
|
||||
"string-to-color": "^2.1.4",
|
||||
"v-click-outside": "^3.0.1",
|
||||
"vue": "^2.6.11",
|
||||
"spotify-web-api-js": "^1.5.0",
|
||||
"string-to-color": "^2.2.2",
|
||||
"v-click-outside": "^3.1.2",
|
||||
"vue": "^2.6.12",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-observe-visibility": "^0.4.6",
|
||||
"vue-progressbar": "^0.7.5",
|
||||
"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",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuedraggable": "^2.24.1",
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.4.6",
|
||||
"@vue/cli-plugin-eslint": "^4.4.6",
|
||||
"@vue/cli-service": "^4.4.6",
|
||||
"@vue/cli-plugin-babel": "^4.5.6",
|
||||
"@vue/cli-plugin-eslint": "^4.5.6",
|
||||
"@vue/cli-service": "^4.5.6",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.3.1",
|
||||
"eslint": "^7.9.0",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"sass": "^1.26.9",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"sass": "^1.26.11",
|
||||
"sass-loader": "^10.0.2",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
},
|
||||
"license": "GPL-2.0"
|
||||
}
|
||||
|
@ -4,11 +4,11 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<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">
|
||||
<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">
|
||||
</head>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<figure>
|
||||
<img v-lazyload
|
||||
:src="dataURI"
|
||||
:data-src="artwork_url_with_size"
|
||||
:data-err="dataURI"
|
||||
@click="$emit('click')">
|
||||
@ -15,7 +14,7 @@ import stringToColor from 'string-to-color'
|
||||
|
||||
export default {
|
||||
name: 'CoverArtwork',
|
||||
props: ['artist', 'album', 'artwork_url'],
|
||||
props: ['artist', 'album', 'artwork_url', 'maxwidth', 'maxheight'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -30,6 +29,9 @@ export default {
|
||||
|
||||
computed: {
|
||||
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)
|
||||
},
|
||||
|
||||
|
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>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
<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 style="margin-top:0.7rem;">
|
||||
<h1 class="title is-6">{{ props.album.name }}</h1>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<h1 class="title is-6">{{ props.artist.name }}</h1>
|
||||
</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">
|
||||
<a class="has-text-link" @click="open_album">{{ album.name }}</a>
|
||||
</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="$emit('remove_podcast')">Remove podcast</a>
|
||||
<a class="button is-small" @click="$emit('remove-podcast')">Remove podcast</a>
|
||||
</div>
|
||||
<div class="content is-small">
|
||||
<p v-if="album.artist && media_kind !== 'audiobook'">
|
||||
<p v-if="album.artist">
|
||||
<span class="heading">Album artist</span>
|
||||
<a class="title is-6 has-text-link" @click="open_artist">{{ album.artist }}</a>
|
||||
</p>
|
||||
<p v-if="album.artist && media_kind === 'audiobook'">
|
||||
<span class="heading">Album artist</span>
|
||||
<span class="title is-6">{{ album.artist }}</span>
|
||||
<p v-if="album.date_released">
|
||||
<span class="heading">Release date</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>
|
||||
<span class="heading">Tracks</span>
|
||||
<span class="title is-6">{{ album.track_count }}</span>
|
||||
</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>
|
||||
<footer class="card-footer">
|
||||
@ -70,6 +86,10 @@ export default {
|
||||
computed: {
|
||||
artwork_url: function () {
|
||||
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 () {
|
||||
if (this.media_kind === 'podcast') {
|
||||
if (this.media_kind_resolved === 'podcast') {
|
||||
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 })
|
||||
} else {
|
||||
this.$router.push({ path: '/music/albums/' + this.album.id })
|
||||
@ -100,7 +120,13 @@ export default {
|
||||
},
|
||||
|
||||
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 () {
|
||||
|
@ -18,6 +18,14 @@
|
||||
<span class="heading">Tracks</span>
|
||||
<span class="title is-6">{{ artist.track_count }}</span>
|
||||
</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>
|
||||
<footer class="card-footer">
|
||||
|
@ -44,7 +44,7 @@ import webapi from '@/webapi'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogPlaylist',
|
||||
props: ['show', 'playlist'],
|
||||
props: ['show', 'playlist', 'tracks'],
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
|
@ -81,7 +81,7 @@
|
||||
</div>
|
||||
<div class="level-item 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
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
@ -174,7 +174,7 @@
|
||||
</div>
|
||||
<div class="level-item 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
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
|
@ -1,22 +1,25 @@
|
||||
<template>
|
||||
<nav class="fd-top-navbar navbar is-light is-fixed-top" :style="zindex" role="navigation" aria-label="main navigation">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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/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/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="/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="/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="/search"><span class="icon"><i class="mdi mdi-magnify"></i></span> <b>Search</b></navbar-item-link>
|
||||
<hr class="fd-navbar-divider">
|
||||
|
||||
<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>
|
||||
@ -87,6 +90,28 @@ export default {
|
||||
},
|
||||
|
||||
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 () {
|
||||
return this.$store.state.player
|
||||
},
|
||||
|
@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<template functional>
|
||||
<div class="media">
|
||||
<div class="media-content fd-has-action is-clipped" v-on:click="open_album">
|
||||
<h1 class="title is-6">{{ album.name }}</h1>
|
||||
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artists[0].name }}</b></h2>
|
||||
<h2 class="subtitle is-7 has-text-grey has-text-weight-normal">({{ album.album_type }}, {{ album.release_date | time('L') }})</h2>
|
||||
<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">
|
||||
<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 class="media-right">
|
||||
<slot name="actions"></slot>
|
||||
@ -14,14 +19,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'SpotifyListItemAlbum',
|
||||
|
||||
props: ['album'],
|
||||
|
||||
methods: {
|
||||
open_album: function () {
|
||||
this.$router.push({ path: '/music/spotify/albums/' + this.album.id })
|
||||
}
|
||||
}
|
||||
props: ['album']
|
||||
}
|
||||
</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>
|
||||
</a>
|
||||
</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">
|
||||
<a>
|
||||
<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 vClickOutside from 'v-click-outside'
|
||||
import VueTinyLazyloadImg from 'vue-tiny-lazyload-img'
|
||||
import VueObserveVisibility from 'vue-observe-visibility'
|
||||
import VueScrollTo from 'vue-scrollto'
|
||||
import 'mdi/css/materialdesignicons.css'
|
||||
import 'vue-range-slider/dist/vue-range-slider.css'
|
||||
import './mystyles.scss'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
Vue.use(vClickOutside)
|
||||
Vue.use(VueTinyLazyloadImg)
|
||||
Vue.use(VueObserveVisibility)
|
||||
Vue.use(VueScrollTo)
|
||||
|
||||
/* eslint-disable no-new */
|
||||
new Vue({
|
||||
|
@ -1,5 +1,6 @@
|
||||
|
||||
@import 'bulma';
|
||||
@import '~bulma-switch';
|
||||
|
||||
|
||||
.slider {
|
||||
@ -80,7 +81,9 @@ a.navbar-item {
|
||||
|
||||
.fd-is-square .button {
|
||||
height: 27px;
|
||||
width: 27px;
|
||||
min-width: 27px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Set minimum height to hide "option" section */
|
||||
.fd-content-with-option {
|
||||
min-height: calc(100vh - 3.25rem - 3.25rem - 5rem);
|
||||
}
|
||||
|
||||
/* Now playing page */
|
||||
.fd-is-fullheight {
|
||||
height: calc(100vh - 3.25rem - 3.25rem);
|
||||
|
@ -26,7 +26,7 @@
|
||||
<!-- Right side -->
|
||||
<div class="level-right">
|
||||
<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="buttons has-addons">
|
||||
<a @click="update" class="button is-small">Update</a>
|
||||
@ -126,6 +126,10 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickOutside (event) {
|
||||
this.show_update_dropdown = false
|
||||
},
|
||||
|
||||
update: function () {
|
||||
this.show_update_dropdown = false
|
||||
webapi.library_update()
|
||||
|
@ -24,14 +24,7 @@
|
||||
</template>
|
||||
<template slot="content">
|
||||
<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)">
|
||||
<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" />
|
||||
<list-tracks :tracks="tracks" :uris="album.uri"></list-tracks>
|
||||
<modal-dialog-album :show="show_album_details_modal" :album="album" @close="show_album_details_modal = false" />
|
||||
</template>
|
||||
</content-with-hero>
|
||||
@ -40,8 +33,7 @@
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHero from '@/templates/ContentWithHero'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ListTracks from '@/components/ListTracks'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import CoverArtwork from '@/components/CoverArtwork'
|
||||
import webapi from '@/webapi'
|
||||
@ -63,16 +55,13 @@ const albumData = {
|
||||
export default {
|
||||
name: 'PageAlbum',
|
||||
mixins: [LoadDataBeforeEnterMixin(albumData)],
|
||||
components: { ContentWithHero, ListItemTrack, ModalDialogTrack, ModalDialogAlbum, CoverArtwork },
|
||||
components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork },
|
||||
|
||||
data () {
|
||||
return {
|
||||
album: {},
|
||||
tracks: [],
|
||||
|
||||
show_details_modal: false,
|
||||
selected_track: {},
|
||||
|
||||
show_album_details_modal: false
|
||||
}
|
||||
},
|
||||
@ -85,15 +74,6 @@ export default {
|
||||
|
||||
play: function () {
|
||||
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>
|
||||
<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 slot="heading-left">
|
||||
<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 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 slot="content">
|
||||
<list-item-album v-for="album in albums_filtered"
|
||||
: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" />
|
||||
<list-albums :albums="albums_list"></list-albums>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@ -40,10 +48,11 @@ import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import IndexButtonList from '@/components/IndexButtonList'
|
||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ListAlbums from '@/components/ListAlbums'
|
||||
import DropdownMenu from '@/components/DropdownMenu'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
import Albums from '@/lib/Albums'
|
||||
|
||||
const albumsData = {
|
||||
load: function (to) {
|
||||
@ -61,48 +70,60 @@ const albumsData = {
|
||||
export default {
|
||||
name: 'PageAlbums',
|
||||
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemAlbum, ModalDialogAlbum },
|
||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListAlbums, DropdownMenu },
|
||||
|
||||
data () {
|
||||
return {
|
||||
albums: { items: [] },
|
||||
index_list: [],
|
||||
|
||||
show_details_modal: false,
|
||||
selected_album: {}
|
||||
sort_options: ['Name', 'Recently added', 'Recently released']
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hide_singles () {
|
||||
return this.$store.state.hide_singles
|
||||
albums_list () {
|
||||
return new Albums(this.albums.items, {
|
||||
hideSingles: this.hide_singles,
|
||||
hideSpotify: this.hide_spotify,
|
||||
sort: this.sort,
|
||||
group: true
|
||||
})
|
||||
},
|
||||
|
||||
albums_filtered () {
|
||||
return this.albums.items.filter(album => !this.hide_singles || album.track_count > 2)
|
||||
spotify_enabled () {
|
||||
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: {
|
||||
update_hide_singles: function (e) {
|
||||
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles)
|
||||
},
|
||||
|
||||
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()))]
|
||||
scrollToTop: function () {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,14 +15,7 @@
|
||||
</template>
|
||||
<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>
|
||||
<list-item-album v-for="album in albums.items" :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" />
|
||||
<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>
|
||||
@ -31,8 +24,7 @@
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ListAlbums from '@/components/ListAlbums'
|
||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
@ -53,16 +45,13 @@ const artistData = {
|
||||
export default {
|
||||
name: 'PageArtist',
|
||||
mixins: [LoadDataBeforeEnterMixin(artistData)],
|
||||
components: { ContentWithHeading, ListItemAlbum, ModalDialogAlbum, ModalDialogArtist },
|
||||
components: { ContentWithHeading, ListAlbums, ModalDialogArtist },
|
||||
|
||||
data () {
|
||||
return {
|
||||
artist: {},
|
||||
albums: {},
|
||||
|
||||
show_details_modal: false,
|
||||
selected_album: {},
|
||||
|
||||
show_artist_details_modal: false
|
||||
}
|
||||
},
|
||||
@ -74,15 +63,6 @@ export default {
|
||||
|
||||
play: function () {
|
||||
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 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>
|
||||
<list-item-track v-for="(track, index) in tracks.items" :key="track.id" :track="track" @click="play_track(index)">
|
||||
<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" />
|
||||
<list-tracks :tracks="tracks.items" :uris="track_uris"></list-tracks>
|
||||
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
@ -37,8 +30,7 @@
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import IndexButtonList from '@/components/IndexButtonList'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ListTracks from '@/components/ListTracks'
|
||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
@ -59,16 +51,13 @@ const tracksData = {
|
||||
export default {
|
||||
name: 'PageArtistTracks',
|
||||
mixins: [LoadDataBeforeEnterMixin(tracksData)],
|
||||
components: { ContentWithHeading, ListItemTrack, IndexButtonList, ModalDialogTrack, ModalDialogArtist },
|
||||
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogArtist },
|
||||
|
||||
data () {
|
||||
return {
|
||||
artist: {},
|
||||
tracks: { items: [] },
|
||||
|
||||
show_details_modal: false,
|
||||
selected_track: {},
|
||||
|
||||
show_artist_details_modal: false
|
||||
}
|
||||
},
|
||||
@ -77,6 +66,10 @@ export default {
|
||||
index_list () {
|
||||
return [...new Set(this.tracks.items
|
||||
.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 () {
|
||||
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>
|
||||
<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 slot="heading-left">
|
||||
<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 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 slot="content">
|
||||
<list-item-artist v-for="artist in artists_filtered"
|
||||
: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" />
|
||||
<list-artists :artists="artists_list"></list-artists>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@ -40,14 +48,15 @@ import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import IndexButtonList from '@/components/IndexButtonList'
|
||||
import ListItemArtist from '@/components/ListItemArtist'
|
||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||
import ListArtists from '@/components/ListArtists'
|
||||
import DropdownMenu from '@/components/DropdownMenu'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
import Artists from '@/lib/Artists'
|
||||
|
||||
const artistsData = {
|
||||
load: function (to) {
|
||||
return webapi.library_artists()
|
||||
return webapi.library_artists('music')
|
||||
},
|
||||
|
||||
set: function (vm, response) {
|
||||
@ -58,45 +67,60 @@ const artistsData = {
|
||||
export default {
|
||||
name: 'PageArtists',
|
||||
mixins: [LoadDataBeforeEnterMixin(artistsData)],
|
||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemArtist, ModalDialogArtist },
|
||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListArtists, DropdownMenu },
|
||||
|
||||
data () {
|
||||
return {
|
||||
artists: { items: [] },
|
||||
|
||||
show_details_modal: false,
|
||||
selected_artist: {}
|
||||
sort_options: ['Name', 'Recently added']
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hide_singles () {
|
||||
return this.$store.state.hide_singles
|
||||
artists_list () {
|
||||
return new Artists(this.artists.items, {
|
||||
hideSingles: this.hide_singles,
|
||||
hideSpotify: this.hide_spotify,
|
||||
sort: this.sort,
|
||||
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()))]
|
||||
spotify_enabled () {
|
||||
return this.$store.state.spotify.webapi_token_valid
|
||||
},
|
||||
|
||||
artists_filtered () {
|
||||
return this.artists.items.filter(artist => !this.hide_singles || artist.track_count > (artist.album_count * 2))
|
||||
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.artists_sort
|
||||
},
|
||||
set (value) {
|
||||
this.$store.commit(types.ARTISTS_SORT, value)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
scrollToTop: function () {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
<content-with-heading>
|
||||
<content-with-hero>
|
||||
<template slot="heading-left">
|
||||
<div class="title is-4">{{ album.name }}</div>
|
||||
<div class="title is-4 has-text-grey has-text-weight-normal">{{ album.artist }}</div>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<div class="buttons is-centered">
|
||||
<h1 class="title is-5">{{ album.name }}</h1>
|
||||
<h2 class="subtitle is-6 has-text-link has-text-weight-normal"><a class="has-text-link" @click="open_artist">{{ album.artist }}</a></h2>
|
||||
|
||||
<div class="buttons fd-is-centered-mobile fd-has-margin-top">
|
||||
<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">
|
||||
<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>Play</span>
|
||||
</a>
|
||||
</div>
|
||||
</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">
|
||||
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
|
||||
<list-item-track v-for="(track, index) in tracks" :key="track.id" :track="track" @click="play_track(index)">
|
||||
<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" />
|
||||
<p class="heading is-7 has-text-centered-mobile fd-has-margin-top">{{ album.track_count }} tracks</p>
|
||||
<list-tracks :tracks="tracks" :uris="album.uri"></list-tracks>
|
||||
<modal-dialog-album :show="show_album_details_modal" :album="album" :media_kind="'audiobook'" @close="show_album_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</content-with-hero>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ContentWithHero from '@/templates/ContentWithHero'
|
||||
import ListTracks from '@/components/ListTracks'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import CoverArtwork from '@/components/CoverArtwork'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const albumData = {
|
||||
@ -55,23 +53,25 @@ const albumData = {
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'PageAudiobook',
|
||||
name: 'PageAudiobooksAlbum',
|
||||
mixins: [LoadDataBeforeEnterMixin(albumData)],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
|
||||
components: { ContentWithHero, ListTracks, ModalDialogAlbum, CoverArtwork },
|
||||
|
||||
data () {
|
||||
return {
|
||||
album: {},
|
||||
tracks: [],
|
||||
|
||||
show_details_modal: false,
|
||||
selected_track: {},
|
||||
|
||||
show_album_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open_artist: function () {
|
||||
this.show_details_modal = false
|
||||
this.$router.push({ path: '/audiobooks/artists/' + this.album.artist_id })
|
||||
},
|
||||
|
||||
play: function () {
|
||||
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>
|
||||
<div>
|
||||
<tabs-music></tabs-music>
|
||||
<tabs-audiobooks></tabs-audiobooks>
|
||||
|
||||
<content-with-heading>
|
||||
<template slot="options">
|
||||
<index-button-list :index="index_list"></index-button-list>
|
||||
<index-button-list :index="artists_list.indexList"></index-button-list>
|
||||
</template>
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">Artists</p>
|
||||
<p class="heading">{{ artists.total }} artists</p>
|
||||
<p class="title is-4">Authors</p>
|
||||
<p class="heading">{{ artists_list.sortedAndFiltered.length }} Authors</p>
|
||||
</template>
|
||||
<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 slot="content">
|
||||
<list-item-artist v-for="artist in artists_filtered"
|
||||
: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" />
|
||||
<list-artists :artists="artists_list"></list-artists>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@ -38,16 +22,15 @@
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import TabsAudiobooks from '@/components/TabsAudiobooks'
|
||||
import IndexButtonList from '@/components/IndexButtonList'
|
||||
import ListItemArtist from '@/components/ListItemArtist'
|
||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||
import ListArtists from '@/components/ListArtists'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
import Artists from '@/lib/Artists'
|
||||
|
||||
const artistsData = {
|
||||
load: function (to) {
|
||||
return webapi.library_artists()
|
||||
return webapi.library_artists('audiobook')
|
||||
},
|
||||
|
||||
set: function (vm, response) {
|
||||
@ -56,48 +39,26 @@ const artistsData = {
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'PageArtists',
|
||||
name: 'PageAudiobooksArtists',
|
||||
mixins: [LoadDataBeforeEnterMixin(artistsData)],
|
||||
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemArtist, ModalDialogArtist },
|
||||
components: { ContentWithHeading, TabsAudiobooks, IndexButtonList, ListArtists },
|
||||
|
||||
data () {
|
||||
return {
|
||||
artists: { items: [] },
|
||||
|
||||
show_details_modal: false,
|
||||
selected_artist: {}
|
||||
artists: { items: [] }
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hide_singles () {
|
||||
return this.$store.state.hide_singles
|
||||
},
|
||||
|
||||
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))
|
||||
artists_list () {
|
||||
return new Artists(this.artists.items, {
|
||||
sort: 'Name',
|
||||
group: true
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
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>
|
||||
|
@ -9,14 +9,7 @@
|
||||
<p class="heading">albums</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-album v-for="album in recently_added.items" :key="album.id" :album="album" @click="open_album(album)">
|
||||
<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" />
|
||||
<list-albums :albums="recently_added.items"></list-albums>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<nav class="level">
|
||||
@ -34,14 +27,7 @@
|
||||
<p class="heading">tracks</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-track v-for="track in recently_played.items" :key="track.id" :track="track" @click="play_track(track)">
|
||||
<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" />
|
||||
<list-tracks :tracks="recently_played.items"></list-tracks>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<nav class="level">
|
||||
@ -58,10 +44,8 @@
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ListAlbums from '@/components/ListAlbums'
|
||||
import ListTracks from '@/components/ListTracks'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const browseData = {
|
||||
@ -81,7 +65,7 @@ const browseData = {
|
||||
export default {
|
||||
name: 'PageBrowse',
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemAlbum, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
|
||||
components: { ContentWithHeading, TabsMusic, ListAlbums, ListTracks },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -89,34 +73,13 @@ export default {
|
||||
recently_played: {},
|
||||
|
||||
show_track_details_modal: false,
|
||||
selected_track: {},
|
||||
|
||||
show_album_details_modal: false,
|
||||
selected_album: {}
|
||||
selected_track: {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open_browse: function (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>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-album v-for="album in recently_added.items" :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" />
|
||||
<list-albums :albums="recently_added.items"></list-albums>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@ -25,8 +18,7 @@
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ListAlbums from '@/components/ListAlbums'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const browseData = {
|
||||
@ -46,25 +38,11 @@ const browseData = {
|
||||
export default {
|
||||
name: 'PageBrowseType',
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemAlbum, ModalDialogAlbum },
|
||||
components: { ContentWithHeading, TabsMusic, ListAlbums },
|
||||
|
||||
data () {
|
||||
return {
|
||||
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
|
||||
recently_added: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,14 +8,7 @@
|
||||
<p class="heading">tracks</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-track v-for="track in recently_played.items" :key="track.id" :track="track" @click="play_track(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" />
|
||||
<list-tracks :tracks="recently_played.items"></list-tracks>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@ -25,8 +18,7 @@
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ListTracks from '@/components/ListTracks'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const browseData = {
|
||||
@ -46,25 +38,11 @@ const browseData = {
|
||||
export default {
|
||||
name: 'PageBrowseType',
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, ListItemTrack, ModalDialogTrack },
|
||||
components: { ContentWithHeading, TabsMusic, ListTracks },
|
||||
|
||||
data () {
|
||||
return {
|
||||
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)
|
||||
recently_played: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,14 +19,7 @@
|
||||
</template>
|
||||
<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>
|
||||
<list-item-albums v-for="album in genre_albums.items" :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-albums>
|
||||
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
|
||||
<list-albums :albums="genre_albums.items"></list-albums>
|
||||
<modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': name }" @close="show_genre_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
@ -37,8 +30,7 @@
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import IndexButtonList from '@/components/IndexButtonList'
|
||||
import ListItemAlbums from '@/components/ListItemAlbum'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ListAlbums from '@/components/ListAlbums'
|
||||
import ModalDialogGenre from '@/components/ModalDialogGenre'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
@ -56,16 +48,13 @@ const genreData = {
|
||||
export default {
|
||||
name: 'PageGenre',
|
||||
mixins: [LoadDataBeforeEnterMixin(genreData)],
|
||||
components: { ContentWithHeading, IndexButtonList, ListItemAlbums, ModalDialogAlbum, ModalDialogGenre },
|
||||
components: { ContentWithHeading, IndexButtonList, ListAlbums, ModalDialogGenre },
|
||||
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
genre_albums: { items: [] },
|
||||
|
||||
show_details_modal: false,
|
||||
selected_album: {},
|
||||
|
||||
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)
|
||||
},
|
||||
|
||||
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 slot="content">
|
||||
<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)">
|
||||
<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" />
|
||||
<list-tracks :tracks="tracks.items" :expression="expression"></list-tracks>
|
||||
<modal-dialog-genre :show="show_genre_details_modal" :genre="{ 'name': genre }" @close="show_genre_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
@ -37,8 +30,7 @@
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import IndexButtonList from '@/components/IndexButtonList'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ListTracks from '@/components/ListTracks'
|
||||
import ModalDialogGenre from '@/components/ModalDialogGenre'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
@ -56,16 +48,13 @@ const tracksData = {
|
||||
export default {
|
||||
name: 'PageGenreTracks',
|
||||
mixins: [LoadDataBeforeEnterMixin(tracksData)],
|
||||
components: { ContentWithHeading, ListItemTrack, IndexButtonList, ModalDialogTrack, ModalDialogGenre },
|
||||
components: { ContentWithHeading, ListTracks, IndexButtonList, ModalDialogGenre },
|
||||
|
||||
data () {
|
||||
return {
|
||||
tracks: { items: [] },
|
||||
genre: '',
|
||||
|
||||
show_details_modal: false,
|
||||
selected_track: {},
|
||||
|
||||
show_genre_details_modal: false
|
||||
}
|
||||
},
|
||||
@ -74,6 +63,10 @@ export default {
|
||||
index_list () {
|
||||
return [...new Set(this.tracks.items
|
||||
.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 () {
|
||||
webapi.player_play_expression('genre is "' + this.genre + '" and media_kind is music', 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
|
||||
webapi.player_play_expression(this.expression, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,15 +15,8 @@
|
||||
</template>
|
||||
<template slot="content">
|
||||
<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)">
|
||||
<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-playlist :show="show_playlist_details_modal" :playlist="playlist" @close="show_playlist_details_modal = false" />
|
||||
<list-tracks :tracks="tracks" :uris="uris"></list-tracks>
|
||||
<modal-dialog-playlist :show="show_playlist_details_modal" :playlist="playlist" :tracks="playlist.random ? tracks : undefined" @close="show_playlist_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
@ -31,8 +24,7 @@
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ListTracks from '@/components/ListTracks'
|
||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
@ -53,32 +45,29 @@ const playlistData = {
|
||||
export default {
|
||||
name: 'PagePlaylist',
|
||||
mixins: [LoadDataBeforeEnterMixin(playlistData)],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogPlaylist },
|
||||
components: { ContentWithHeading, ListTracks, ModalDialogPlaylist },
|
||||
|
||||
data () {
|
||||
return {
|
||||
playlist: {},
|
||||
tracks: [],
|
||||
|
||||
show_details_modal: false,
|
||||
selected_track: {},
|
||||
|
||||
show_playlist_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
uris () {
|
||||
if (this.playlist.random) {
|
||||
return this.tracks.map(a => a.uri).join(',')
|
||||
}
|
||||
return this.playlist.uri
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
play: function () {
|
||||
webapi.player_play_uri(this.playlist.uri, 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
|
||||
webapi.player_play_uri(this.uris, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,19 +5,7 @@
|
||||
<p class="heading">{{ playlists.total }} playlists</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-playlist v-for="playlist in playlists.items" :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" />
|
||||
<list-playlists :playlists="playlists.items"></list-playlists>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
@ -25,8 +13,7 @@
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemPlaylist from '@/components/ListItemPlaylist'
|
||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
||||
import ListPlaylists from '@/components/ListPlaylists'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const playlistsData = {
|
||||
@ -46,30 +33,12 @@ const playlistsData = {
|
||||
export default {
|
||||
name: 'PagePlaylists',
|
||||
mixins: [LoadDataBeforeEnterMixin(playlistsData)],
|
||||
components: { ContentWithHeading, ListItemPlaylist, ModalDialogPlaylist },
|
||||
components: { ContentWithHeading, ListPlaylists },
|
||||
|
||||
data () {
|
||||
return {
|
||||
playlist: {},
|
||||
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
|
||||
playlists: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,35 +52,14 @@
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" :media_kind="'podcast'" @click="open_album(album)">
|
||||
<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"
|
||||
: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>
|
||||
<list-albums :albums="albums.items"
|
||||
@play_count_changed="reload_new_episodes()"
|
||||
@podcast-deleted="reload_podcasts()">
|
||||
</list-albums>
|
||||
<modal-dialog-add-rss
|
||||
:show="show_url_modal"
|
||||
@close="show_url_modal = false"
|
||||
@podcast_added="reload_podcasts" />
|
||||
:show="show_url_modal"
|
||||
@close="show_url_modal = false"
|
||||
@podcast_added="reload_podcasts()" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@ -90,11 +69,9 @@
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||
import ListAlbums from '@/components/ListAlbums'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ModalDialogAddRss from '@/components/ModalDialogAddRss'
|
||||
import ModalDialog from '@/components/ModalDialog'
|
||||
import RangeSlider from 'vue-range-slider'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
@ -115,31 +92,21 @@ const albumsData = {
|
||||
export default {
|
||||
name: 'PagePodcasts',
|
||||
mixins: [LoadDataBeforeEnterMixin(albumsData)],
|
||||
components: { ContentWithHeading, ListItemTrack, ListItemAlbum, ModalDialogTrack, ModalDialogAlbum, ModalDialogAddRss, ModalDialog, RangeSlider },
|
||||
components: { ContentWithHeading, ListItemTrack, ListAlbums, ModalDialogTrack, ModalDialogAddRss, RangeSlider },
|
||||
|
||||
data () {
|
||||
return {
|
||||
albums: {},
|
||||
new_episodes: { items: [] },
|
||||
|
||||
show_album_details_modal: false,
|
||||
selected_album: {},
|
||||
|
||||
show_url_modal: false,
|
||||
|
||||
show_track_details_modal: false,
|
||||
selected_track: {},
|
||||
|
||||
show_remove_podcast_modal: false,
|
||||
rss_playlist_to_remove: {}
|
||||
selected_track: {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open_album: function (album) {
|
||||
this.$router.push({ path: '/podcasts/' + album.id })
|
||||
},
|
||||
|
||||
play_track: function (track) {
|
||||
webapi.player_play_uri(track.uri, false)
|
||||
},
|
||||
@ -149,11 +116,6 @@ export default {
|
||||
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 () {
|
||||
this.new_episodes.items.forEach(ep => {
|
||||
webapi.library_track_update(ep.id, { play_count: 'increment' })
|
||||
@ -165,29 +127,6 @@ export default {
|
||||
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 () {
|
||||
webapi.library_podcasts_new_episodes().then(({ data }) => {
|
||||
this.new_episodes = data.tracks
|
||||
|
@ -1,21 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<tabs-music></tabs-music>
|
||||
|
||||
<content-with-heading>
|
||||
<template slot="heading-left">
|
||||
<p class="title is-4">Radio</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<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)">
|
||||
<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" />
|
||||
<list-tracks :tracks="tracks.items"></list-tracks>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@ -23,10 +14,8 @@
|
||||
|
||||
<script>
|
||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ListTracks from '@/components/ListTracks'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const streamsData = {
|
||||
@ -42,25 +31,11 @@ const streamsData = {
|
||||
export default {
|
||||
name: 'PageRadioStreams',
|
||||
mixins: [LoadDataBeforeEnterMixin(streamsData)],
|
||||
components: { TabsMusic, ContentWithHeading, ListItemTrack, ModalDialogTrack },
|
||||
components: { ContentWithHeading, ListTracks },
|
||||
|
||||
data () {
|
||||
return {
|
||||
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
|
||||
tracks: { items: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,19 +34,12 @@
|
||||
<p class="title is-4">Tracks</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-track v-for="track in tracks.items" :key="track.id" :track="track" @click="play_track(track)">
|
||||
<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" />
|
||||
<list-tracks :tracks="tracks.items"></list-tracks>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<nav v-if="show_all_tracks_button" class="level">
|
||||
<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>
|
||||
</nav>
|
||||
<p v-if="!tracks.total">No results</p>
|
||||
@ -59,19 +52,12 @@
|
||||
<p class="title is-4">Artists</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-artist v-for="artist in artists.items" :key="artist.id" :artist="artist" @click="open_artist(artist)">
|
||||
<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" />
|
||||
<list-artists :artists="artists.items"></list-artists>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<nav v-if="show_all_artists_button" class="level">
|
||||
<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>
|
||||
</nav>
|
||||
<p v-if="!artists.total">No results</p>
|
||||
@ -84,19 +70,12 @@
|
||||
<p class="title is-4">Albums</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" @click="open_album(album)">
|
||||
<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" />
|
||||
<list-albums :albums="albums.items"></list-albums>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<nav v-if="show_all_albums_button" class="level">
|
||||
<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>
|
||||
</nav>
|
||||
<p v-if="!albums.total">No results</p>
|
||||
@ -109,44 +88,69 @@
|
||||
<p class="title is-4">Playlists</p>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<list-item-playlist v-for="playlist in playlists.items" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
|
||||
<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" />
|
||||
<list-playlists :playlists="playlists.items"></list-playlists>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<nav v-if="show_all_playlists_button" class="level">
|
||||
<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>
|
||||
</nav>
|
||||
<p v-if="!playlists.total">No results</p>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsSearch from '@/components/TabsSearch'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ListItemArtist from '@/components/ListItemArtist'
|
||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||
import ListItemPlaylist from '@/components/ListItemPlaylist'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
||||
import ListTracks from '@/components/ListTracks'
|
||||
import ListArtists from '@/components/ListArtists'
|
||||
import ListAlbums from '@/components/ListAlbums'
|
||||
import ListPlaylists from '@/components/ListPlaylists'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
|
||||
export default {
|
||||
name: 'PageSearch',
|
||||
components: { ContentWithHeading, TabsSearch, ListItemTrack, ListItemArtist, ListItemAlbum, ListItemPlaylist, ModalDialogTrack, ModalDialogAlbum, ModalDialogArtist, ModalDialogPlaylist },
|
||||
components: { ContentWithHeading, TabsSearch, ListTracks, ListArtists, ListAlbums, ListPlaylists },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -156,18 +160,8 @@ export default {
|
||||
artists: { items: [], total: 0 },
|
||||
albums: { items: [], total: 0 },
|
||||
playlists: { items: [], total: 0 },
|
||||
|
||||
show_track_details_modal: false,
|
||||
selected_track: {},
|
||||
|
||||
show_album_details_modal: false,
|
||||
selected_album: {},
|
||||
|
||||
show_artist_details_modal: false,
|
||||
selected_artist: {},
|
||||
|
||||
show_playlist_details_modal: false,
|
||||
selected_playlist: {}
|
||||
audiobooks: { items: [], total: 0 },
|
||||
podcasts: { items: [], total: 0 }
|
||||
}
|
||||
},
|
||||
|
||||
@ -202,6 +196,24 @@ export default {
|
||||
},
|
||||
show_all_playlists_button () {
|
||||
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
|
||||
}
|
||||
|
||||
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 = {
|
||||
type: route.query.type,
|
||||
type: query.type,
|
||||
media_kind: 'music'
|
||||
}
|
||||
|
||||
if (route.query.query.startsWith('query:')) {
|
||||
searchParams.expression = route.query.query.replace(/^query:/, '').trim()
|
||||
if (query.query.startsWith('query:')) {
|
||||
searchParams.expression = query.query.replace(/^query:/, '').trim()
|
||||
} else {
|
||||
searchParams.query = route.query.query
|
||||
searchParams.query = query.query
|
||||
}
|
||||
|
||||
if (route.query.limit) {
|
||||
searchParams.limit = route.query.limit
|
||||
searchParams.offset = route.query.offset
|
||||
if (query.limit) {
|
||||
searchParams.limit = query.limit
|
||||
searchParams.offset = query.offset
|
||||
}
|
||||
|
||||
webapi.search(searchParams).then(({ data }) => {
|
||||
@ -234,8 +257,58 @@ export default {
|
||||
this.artists = data.artists ? data.artists : { items: [], total: 0 }
|
||||
this.albums = data.albums ? data.albums : { 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({
|
||||
path: '/search/library',
|
||||
query: {
|
||||
type: 'track,artist,album,playlist',
|
||||
type: 'track,artist,album,playlist,audiobook,podcast',
|
||||
query: this.search_query,
|
||||
limit: 3,
|
||||
offset: 0
|
||||
@ -296,45 +369,29 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
play_track: function (track) {
|
||||
webapi.player_play_uri(track.uri, false)
|
||||
open_search_audiobooks: function () {
|
||||
this.$router.push({
|
||||
path: '/search/library',
|
||||
query: {
|
||||
type: 'audiobook',
|
||||
query: this.$route.query.query
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
open_artist: function (artist) {
|
||||
this.$router.push({ path: '/music/artists/' + artist.id })
|
||||
},
|
||||
|
||||
open_album: function (album) {
|
||||
this.$router.push({ path: '/music/albums/' + album.id })
|
||||
},
|
||||
|
||||
open_playlist: function (playlist) {
|
||||
this.$router.push({ path: '/playlists/' + playlist.id + '/tracks' })
|
||||
open_search_podcasts: function () {
|
||||
this.$router.push({
|
||||
path: '/search/library',
|
||||
query: {
|
||||
type: 'podcast',
|
||||
query: this.$route.query.query
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
open_recent_search: function (query) {
|
||||
this.search_query = query
|
||||
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>
|
||||
<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>
|
||||
<template slot="heading-left">
|
||||
<div class="title is-4">Now playing page</div>
|
||||
|
@ -15,7 +15,20 @@
|
||||
</template>
|
||||
<template slot="content">
|
||||
<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">
|
||||
<a @click="open_dialog(album)">
|
||||
<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 SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
||||
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist'
|
||||
import CoverArtwork from '@/components/CoverArtwork'
|
||||
import store from '@/store'
|
||||
import webapi from '@/webapi'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
@ -63,7 +77,7 @@ const artistData = {
|
||||
export default {
|
||||
name: 'SpotifyPageArtist',
|
||||
mixins: [LoadDataBeforeEnterMixin(artistData)],
|
||||
components: { ContentWithHeading, SpotifyListItemAlbum, SpotifyModalDialogAlbum, SpotifyModalDialogArtist, InfiniteLoading },
|
||||
components: { ContentWithHeading, SpotifyListItemAlbum, SpotifyModalDialogAlbum, SpotifyModalDialogArtist, InfiniteLoading, CoverArtwork },
|
||||
|
||||
data () {
|
||||
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: {
|
||||
load_next: function ($state) {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
@ -106,9 +126,20 @@ export default {
|
||||
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) {
|
||||
this.selected_album = album
|
||||
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>
|
||||
</template>
|
||||
<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">
|
||||
<a @click="open_album_dialog(album)">
|
||||
<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 SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
||||
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
|
||||
import CoverArtwork from '@/components/CoverArtwork'
|
||||
import store from '@/store'
|
||||
import * as types from '@/store/mutation_types'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
@ -93,7 +107,7 @@ const browseData = {
|
||||
export default {
|
||||
name: 'SpotifyPageBrowse',
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist },
|
||||
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyListItemPlaylist, SpotifyModalDialogAlbum, SpotifyModalDialogPlaylist, CoverArtwork },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -112,10 +126,19 @@ export default {
|
||||
|
||||
featured_playlists () {
|
||||
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: {
|
||||
|
||||
open_album: function (album) {
|
||||
this.$router.push({ path: '/music/spotify/albums/' + album.id })
|
||||
},
|
||||
|
||||
open_album_dialog: function (album) {
|
||||
this.selected_album = album
|
||||
this.show_album_details_modal = true
|
||||
@ -124,6 +147,13 @@ export default {
|
||||
open_playlist_dialog: function (playlist) {
|
||||
this.selected_playlist = playlist
|
||||
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>
|
||||
</template>
|
||||
<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">
|
||||
<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>
|
||||
</a>
|
||||
</template>
|
||||
@ -26,6 +39,7 @@ import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import TabsMusic from '@/components/TabsMusic'
|
||||
import SpotifyListItemAlbum from '@/components/SpotifyListItemAlbum'
|
||||
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
||||
import CoverArtwork from '@/components/CoverArtwork'
|
||||
import store from '@/store'
|
||||
import * as types from '@/store/mutation_types'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
@ -51,7 +65,7 @@ const browseData = {
|
||||
export default {
|
||||
name: 'SpotifyPageBrowseNewReleases',
|
||||
mixins: [LoadDataBeforeEnterMixin(browseData)],
|
||||
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyModalDialogAlbum },
|
||||
components: { ContentWithHeading, TabsMusic, SpotifyListItemAlbum, SpotifyModalDialogAlbum, CoverArtwork },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -63,13 +77,29 @@ export default {
|
||||
computed: {
|
||||
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: {
|
||||
|
||||
open_album: function (album) {
|
||||
this.$router.push({ path: '/music/spotify/albums/' + album.id })
|
||||
},
|
||||
|
||||
open_album_dialog: function (album) {
|
||||
this.selected_album = album
|
||||
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">
|
||||
<nav v-if="show_all_tracks_button" class="level">
|
||||
<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>
|
||||
</nav>
|
||||
<p v-if="!tracks.total">No results</p>
|
||||
@ -70,7 +70,7 @@
|
||||
<template slot="footer">
|
||||
<nav v-if="show_all_artists_button" class="level">
|
||||
<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>
|
||||
</nav>
|
||||
<p v-if="!artists.total">No results</p>
|
||||
@ -83,7 +83,20 @@
|
||||
<p class="title is-4">Albums</p>
|
||||
</template>
|
||||
<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">
|
||||
<a @click="open_album_dialog(album)">
|
||||
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
|
||||
@ -96,7 +109,7 @@
|
||||
<template slot="footer">
|
||||
<nav v-if="show_all_albums_button" class="level">
|
||||
<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>
|
||||
</nav>
|
||||
<p v-if="!albums.total">No results</p>
|
||||
@ -122,7 +135,7 @@
|
||||
<template slot="footer">
|
||||
<nav v-if="show_all_playlists_button" class="level">
|
||||
<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>
|
||||
</nav>
|
||||
<p v-if="!playlists.total">No results</p>
|
||||
@ -142,6 +155,7 @@ import SpotifyModalDialogTrack from '@/components/SpotifyModalDialogTrack'
|
||||
import SpotifyModalDialogArtist from '@/components/SpotifyModalDialogArtist'
|
||||
import SpotifyModalDialogAlbum from '@/components/SpotifyModalDialogAlbum'
|
||||
import SpotifyModalDialogPlaylist from '@/components/SpotifyModalDialogPlaylist'
|
||||
import CoverArtwork from '@/components/CoverArtwork'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
@ -149,7 +163,7 @@ import InfiniteLoading from 'vue-infinite-loading'
|
||||
|
||||
export default {
|
||||
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 () {
|
||||
return {
|
||||
@ -172,7 +186,9 @@ export default {
|
||||
selected_artist: {},
|
||||
|
||||
show_playlist_details_modal: false,
|
||||
selected_playlist: {}
|
||||
selected_playlist: {},
|
||||
|
||||
validSearchTypes: ['track', 'artist', 'album', 'playlist']
|
||||
}
|
||||
},
|
||||
|
||||
@ -207,6 +223,10 @@ export default {
|
||||
},
|
||||
show_all_playlists_button () {
|
||||
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()
|
||||
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({
|
||||
path: '/search/spotify',
|
||||
query: {
|
||||
type: 'track,artist,album,playlist',
|
||||
type: 'track,artist,album,playlist,audiobook,podcast',
|
||||
query: this.search_query,
|
||||
limit: 3,
|
||||
offset: 0
|
||||
@ -390,6 +411,17 @@ export default {
|
||||
open_playlist_dialog: function (playlist) {
|
||||
this.selected_playlist = playlist
|
||||
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 PagePodcasts from '@/pages/PagePodcasts'
|
||||
import PagePodcast from '@/pages/PagePodcast'
|
||||
import PageAudiobooks from '@/pages/PageAudiobooks'
|
||||
import PageAudiobook from '@/pages/PageAudiobook'
|
||||
import PageAudiobooksAlbums from '@/pages/PageAudiobooksAlbums'
|
||||
import PageAudiobooksArtists from '@/pages/PageAudiobooksArtists'
|
||||
import PageAudiobooksArtist from '@/pages/PageAudiobooksArtist'
|
||||
import PageAudiobooksAlbum from '@/pages/PageAudiobooksAlbum'
|
||||
import PagePlaylists from '@/pages/PagePlaylists'
|
||||
import PagePlaylist from '@/pages/PagePlaylist'
|
||||
import PageFiles from '@/pages/PageFiles'
|
||||
@ -126,12 +128,6 @@ export const router = new VueRouter({
|
||||
component: PageGenreTracks,
|
||||
meta: { show_progress: true, has_index: true }
|
||||
},
|
||||
{
|
||||
path: '/music/radio',
|
||||
name: 'Radio',
|
||||
component: PageRadioStreams,
|
||||
meta: { show_progress: true, has_tabs: true }
|
||||
},
|
||||
{
|
||||
path: '/podcasts',
|
||||
name: 'Podcasts',
|
||||
@ -146,14 +142,36 @@ export const router = new VueRouter({
|
||||
},
|
||||
{
|
||||
path: '/audiobooks',
|
||||
name: 'Audiobooks',
|
||||
component: PageAudiobooks,
|
||||
redirect: '/audiobooks/artists'
|
||||
},
|
||||
{
|
||||
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 }
|
||||
},
|
||||
{
|
||||
path: '/audiobooks/albums',
|
||||
name: 'AudiobooksAlbums',
|
||||
component: PageAudiobooksAlbums,
|
||||
meta: { show_progress: true, has_tabs: true, has_index: true }
|
||||
},
|
||||
{
|
||||
path: '/audiobooks/:album_id',
|
||||
name: 'Audiobook',
|
||||
component: PageAudiobook,
|
||||
component: PageAudiobooksAlbum,
|
||||
meta: { show_progress: true }
|
||||
},
|
||||
{
|
||||
path: '/radio',
|
||||
name: 'Radio',
|
||||
component: PageRadioStreams,
|
||||
meta: { show_progress: true }
|
||||
},
|
||||
{
|
||||
@ -258,11 +276,11 @@ export const router = new VueRouter({
|
||||
}, 10)
|
||||
})
|
||||
} 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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve({ selector: to.hash, offset: { x: 0, y: 90 } })
|
||||
resolve({ selector: to.hash, offset: { x: 0, y: 120 } })
|
||||
}, 10)
|
||||
})
|
||||
} else if (to.meta.has_index) {
|
||||
|
@ -53,6 +53,9 @@ export default new Vuex.Store({
|
||||
recent_searches: [],
|
||||
|
||||
hide_singles: false,
|
||||
hide_spotify: false,
|
||||
artists_sort: 'Name',
|
||||
albums_sort: 'Name',
|
||||
show_only_next_items: false,
|
||||
show_burger_menu: false,
|
||||
show_player_menu: false
|
||||
@ -91,6 +94,18 @@ export default new Vuex.Store({
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
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 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_BURGER_MENU = 'SHOW_BURGER_MENU'
|
||||
export const SHOW_PLAYER_MENU = 'SHOW_PLAYER_MENU'
|
||||
|
@ -3,25 +3,35 @@
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-four-fifths">
|
||||
<slot name="options"></slot>
|
||||
<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>
|
||||
<section v-if="$slots['options']">
|
||||
<div v-observe-visibility="observer_options"></div>
|
||||
<slot name="options"></slot>
|
||||
<nav class="buttons is-centered" style="margin-bottom: 6px; margin-top: 16px;">
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<!-- Right side -->
|
||||
<div class="level-right has-text-centered-mobile">
|
||||
<slot name="heading-right"></slot>
|
||||
<!-- Right side -->
|
||||
<div class="level-right has-text-centered-mobile">
|
||||
<slot name="heading-right"></slot>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<slot name="content"></slot>
|
||||
<div style="margin-top: 16px;">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</nav>
|
||||
<slot name="content"></slot>
|
||||
<div style="margin-top: 16px;">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -30,6 +40,41 @@
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<style>
|
||||
|
@ -12,51 +12,51 @@ axios.interceptors.response.use(function (response) {
|
||||
|
||||
export default {
|
||||
config () {
|
||||
return axios.get('/api/config')
|
||||
return axios.get('./api/config')
|
||||
},
|
||||
|
||||
settings () {
|
||||
return axios.get('/api/settings')
|
||||
return axios.get('./api/settings')
|
||||
},
|
||||
|
||||
settings_update (categoryName, option) {
|
||||
return axios.put('/api/settings/' + categoryName + '/' + option.name, option)
|
||||
return axios.put('./api/settings/' + categoryName + '/' + option.name, option)
|
||||
},
|
||||
|
||||
library_stats () {
|
||||
return axios.get('/api/library')
|
||||
return axios.get('./api/library')
|
||||
},
|
||||
|
||||
library_update () {
|
||||
return axios.put('/api/update')
|
||||
return axios.put('./api/update')
|
||||
},
|
||||
|
||||
library_rescan () {
|
||||
return axios.put('/api/rescan')
|
||||
return axios.put('./api/rescan')
|
||||
},
|
||||
|
||||
library_count (expression) {
|
||||
return axios.get('/api/library/count?expression=' + expression)
|
||||
return axios.get('./api/library/count?expression=' + expression)
|
||||
},
|
||||
|
||||
queue () {
|
||||
return axios.get('/api/queue')
|
||||
return axios.get('./api/queue')
|
||||
},
|
||||
|
||||
queue_clear () {
|
||||
return axios.put('/api/queue/clear')
|
||||
return axios.put('./api/queue/clear')
|
||||
},
|
||||
|
||||
queue_remove (itemId) {
|
||||
return axios.delete('/api/queue/items/' + itemId)
|
||||
return axios.delete('./api/queue/items/' + itemId)
|
||||
},
|
||||
|
||||
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) {
|
||||
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 })
|
||||
return Promise.resolve(response)
|
||||
})
|
||||
@ -67,7 +67,7 @@ export default {
|
||||
if (store.getters.now_playing && store.getters.now_playing.id) {
|
||||
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 })
|
||||
return Promise.resolve(response)
|
||||
})
|
||||
@ -77,7 +77,7 @@ export default {
|
||||
var options = {}
|
||||
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 })
|
||||
return Promise.resolve(response)
|
||||
})
|
||||
@ -91,21 +91,21 @@ export default {
|
||||
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 })
|
||||
return Promise.resolve(response)
|
||||
})
|
||||
},
|
||||
|
||||
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 })
|
||||
return Promise.resolve(response)
|
||||
})
|
||||
},
|
||||
|
||||
player_status () {
|
||||
return axios.get('/api/player')
|
||||
return axios.get('./api/player')
|
||||
},
|
||||
|
||||
player_play_uri (uris, shuffle, position = undefined) {
|
||||
@ -116,7 +116,7 @@ export default {
|
||||
options.playback = 'start'
|
||||
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) {
|
||||
@ -127,111 +127,111 @@ export default {
|
||||
options.playback = 'start'
|
||||
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 = {}) {
|
||||
return axios.put('/api/player/play', undefined, { params: options })
|
||||
return axios.put('./api/player/play', undefined, { params: options })
|
||||
},
|
||||
|
||||
player_playpos (position) {
|
||||
return axios.put('/api/player/play?position=' + position)
|
||||
return axios.put('./api/player/play?position=' + position)
|
||||
},
|
||||
|
||||
player_playid (itemId) {
|
||||
return axios.put('/api/player/play?item_id=' + itemId)
|
||||
return axios.put('./api/player/play?item_id=' + itemId)
|
||||
},
|
||||
|
||||
player_pause () {
|
||||
return axios.put('/api/player/pause')
|
||||
return axios.put('./api/player/pause')
|
||||
},
|
||||
|
||||
player_stop () {
|
||||
return axios.put('/api/player/stop')
|
||||
return axios.put('./api/player/stop')
|
||||
},
|
||||
|
||||
player_next () {
|
||||
return axios.put('/api/player/next')
|
||||
return axios.put('./api/player/next')
|
||||
},
|
||||
|
||||
player_previous () {
|
||||
return axios.put('/api/player/previous')
|
||||
return axios.put('./api/player/previous')
|
||||
},
|
||||
|
||||
player_shuffle (newState) {
|
||||
var shuffle = newState ? 'true' : 'false'
|
||||
return axios.put('/api/player/shuffle?state=' + shuffle)
|
||||
return axios.put('./api/player/shuffle?state=' + shuffle)
|
||||
},
|
||||
|
||||
player_consume (newState) {
|
||||
var consume = newState ? 'true' : 'false'
|
||||
return axios.put('/api/player/consume?state=' + consume)
|
||||
return axios.put('./api/player/consume?state=' + consume)
|
||||
},
|
||||
|
||||
player_repeat (newRepeatMode) {
|
||||
return axios.put('/api/player/repeat?state=' + newRepeatMode)
|
||||
return axios.put('./api/player/repeat?state=' + newRepeatMode)
|
||||
},
|
||||
|
||||
player_volume (volume) {
|
||||
return axios.put('/api/player/volume?volume=' + volume)
|
||||
return axios.put('./api/player/volume?volume=' + volume)
|
||||
},
|
||||
|
||||
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) {
|
||||
return axios.put('/api/player/seek?position_ms=' + newPosition)
|
||||
return axios.put('./api/player/seek?position_ms=' + newPosition)
|
||||
},
|
||||
|
||||
player_seek (seekMs) {
|
||||
return axios.put('/api/player/seek?seek_ms=' + seekMs)
|
||||
return axios.put('./api/player/seek?seek_ms=' + seekMs)
|
||||
},
|
||||
|
||||
outputs () {
|
||||
return axios.get('/api/outputs')
|
||||
return axios.get('./api/outputs')
|
||||
},
|
||||
|
||||
output_update (outputId, output) {
|
||||
return axios.put('/api/outputs/' + outputId, output)
|
||||
return axios.put('./api/outputs/' + outputId, output)
|
||||
},
|
||||
|
||||
output_toggle (outputId) {
|
||||
return axios.put('/api/outputs/' + outputId + '/toggle')
|
||||
return axios.put('./api/outputs/' + outputId + '/toggle')
|
||||
},
|
||||
|
||||
library_artists () {
|
||||
return axios.get('/api/library/artists?media_kind=music')
|
||||
library_artists (media_kind = undefined) {
|
||||
return axios.get('./api/library/artists', { params: { media_kind: media_kind } })
|
||||
},
|
||||
|
||||
library_artist (artistId) {
|
||||
return axios.get('/api/library/artists/' + artistId)
|
||||
return axios.get('./api/library/artists/' + 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) {
|
||||
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) {
|
||||
return axios.get('/api/library/albums/' + albumId)
|
||||
return axios.get('./api/library/albums/' + albumId)
|
||||
},
|
||||
|
||||
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
|
||||
})
|
||||
},
|
||||
|
||||
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 () {
|
||||
return axios.get('/api/library/genres')
|
||||
return axios.get('./api/library/genres')
|
||||
},
|
||||
|
||||
library_genre (genre) {
|
||||
@ -240,7 +240,7 @@ export default {
|
||||
media_kind: 'music',
|
||||
expression: 'genre is "' + genre + '"'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
return axios.get('./api/search', {
|
||||
params: genreParams
|
||||
})
|
||||
},
|
||||
@ -251,7 +251,7 @@ export default {
|
||||
media_kind: 'music',
|
||||
expression: 'genre is "' + genre + '"'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
return axios.get('./api/search', {
|
||||
params: genreParams
|
||||
})
|
||||
},
|
||||
@ -262,7 +262,7 @@ export default {
|
||||
media_kind: 'music',
|
||||
expression: 'data_kind is url and song_length = 0'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
return axios.get('./api/search', {
|
||||
params: params
|
||||
})
|
||||
},
|
||||
@ -273,7 +273,7 @@ export default {
|
||||
type: 'tracks',
|
||||
expression: 'songartistid is "' + artist + '"'
|
||||
}
|
||||
return axios.get('/api/search', {
|
||||
return axios.get('./api/search', {
|
||||
params: artistParams
|
||||
})
|
||||
}
|
||||
@ -284,7 +284,7 @@ export default {
|
||||
type: 'tracks',
|
||||
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
|
||||
})
|
||||
},
|
||||
@ -294,86 +294,86 @@ export default {
|
||||
type: 'tracks',
|
||||
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
|
||||
})
|
||||
},
|
||||
|
||||
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) {
|
||||
return axios.delete('/api/library/playlists/' + playlistId, undefined)
|
||||
return axios.delete('./api/library/playlists/' + playlistId, undefined)
|
||||
},
|
||||
|
||||
library_playlists () {
|
||||
return axios.get('/api/library/playlists')
|
||||
return axios.get('./api/library/playlists')
|
||||
},
|
||||
|
||||
library_playlist_folder (playlistId = 0) {
|
||||
return axios.get('/api/library/playlists/' + playlistId + '/playlists')
|
||||
return axios.get('./api/library/playlists/' + playlistId + '/playlists')
|
||||
},
|
||||
|
||||
library_playlist (playlistId) {
|
||||
return axios.get('/api/library/playlists/' + playlistId)
|
||||
return axios.get('./api/library/playlists/' + playlistId)
|
||||
},
|
||||
|
||||
library_playlist_tracks (playlistId) {
|
||||
return axios.get('/api/library/playlists/' + playlistId + '/tracks')
|
||||
return axios.get('./api/library/playlists/' + playlistId + '/tracks')
|
||||
},
|
||||
|
||||
library_track (trackId) {
|
||||
return axios.get('/api/library/tracks/' + trackId)
|
||||
return axios.get('./api/library/tracks/' + 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 = {}) {
|
||||
return axios.put('/api/library/tracks/' + trackId, undefined, { params: attributes })
|
||||
return axios.put('./api/library/tracks/' + trackId, undefined, { params: attributes })
|
||||
},
|
||||
|
||||
library_files (directory = undefined) {
|
||||
var filesParams = { directory: directory }
|
||||
return axios.get('/api/library/files', {
|
||||
return axios.get('./api/library/files', {
|
||||
params: filesParams
|
||||
})
|
||||
},
|
||||
|
||||
search (searchParams) {
|
||||
return axios.get('/api/search', {
|
||||
return axios.get('./api/search', {
|
||||
params: searchParams
|
||||
})
|
||||
},
|
||||
|
||||
spotify () {
|
||||
return axios.get('/api/spotify')
|
||||
return axios.get('./api/spotify')
|
||||
},
|
||||
|
||||
spotify_login (credentials) {
|
||||
return axios.post('/api/spotify-login', credentials)
|
||||
return axios.post('./api/spotify-login', credentials)
|
||||
},
|
||||
|
||||
lastfm () {
|
||||
return axios.get('/api/lastfm')
|
||||
return axios.get('./api/lastfm')
|
||||
},
|
||||
|
||||
lastfm_login (credentials) {
|
||||
return axios.post('/api/lastfm-login', credentials)
|
||||
return axios.post('./api/lastfm-login', credentials)
|
||||
},
|
||||
|
||||
lastfm_logout (credentials) {
|
||||
return axios.get('/api/lastfm-logout')
|
||||
return axios.get('./api/lastfm-logout')
|
||||
},
|
||||
|
||||
pairing () {
|
||||
return axios.get('/api/pairing')
|
||||
return axios.get('./api/pairing')
|
||||
},
|
||||
|
||||
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) {
|
||||
|
@ -10,6 +10,9 @@ module.exports = {
|
||||
|
||||
assetsDir: 'player',
|
||||
|
||||
// Relative public path
|
||||
publicPath: './',
|
||||
|
||||
// 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
|
||||
// build
|
||||
|
Loading…
Reference in New Issue
Block a user