[htdocs] Add admin web interface
This commit is contained in:
parent
2c51c6571a
commit
b678542ad6
|
@ -8,7 +8,7 @@ sysconf_DATA = $(CONF_FILE)
|
|||
|
||||
BUILT_SOURCES = $(CONF_FILE) $(SYSTEMD_SERVICE_FILE)
|
||||
|
||||
SUBDIRS = sqlext src
|
||||
SUBDIRS = sqlext src htdocs
|
||||
|
||||
dist_man_MANS = forked-daapd.8
|
||||
|
||||
|
|
|
@ -390,6 +390,7 @@ dnl --- End options ---
|
|||
AC_CONFIG_FILES([
|
||||
src/Makefile
|
||||
sqlext/Makefile
|
||||
htdocs/Makefile
|
||||
Makefile
|
||||
forked-daapd.spec
|
||||
])
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
htdocsdir = $(datadir)/forked-daapd/htdocs
|
||||
|
||||
htdocs_DATA = \
|
||||
admin.html
|
||||
|
||||
htdocscssdir = $(datadir)/forked-daapd/htdocs/css
|
||||
|
||||
htdocscss_DATA = \
|
||||
css/bulma.min.css \
|
||||
css/font-awesome.min.css \
|
||||
css/forked-daapd.css
|
||||
|
||||
htdocsfontsdir = $(datadir)/forked-daapd/htdocs/fonts
|
||||
|
||||
htdocsfonts_DATA = \
|
||||
fonts/FontAwesome.otf\
|
||||
fonts/fontawesome-webfont.eot \
|
||||
fonts/fontawesome-webfont.svg \
|
||||
fonts/fontawesome-webfont.ttf \
|
||||
fonts/fontawesome-webfont.woff \
|
||||
fonts/fontawesome-webfont.woff2
|
||||
|
||||
htdocsjsdir = $(datadir)/forked-daapd/htdocs/js
|
||||
|
||||
htdocsjs_DATA = \
|
||||
js/axios.js \
|
||||
js/axios.map \
|
||||
js/axios.min.js \
|
||||
js/axios.min.map \
|
||||
js/forked-daapd.js \
|
||||
js/vue.js \
|
||||
js/vue.min.js
|
|
@ -0,0 +1,198 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>forked-daapd</title>
|
||||
<link rel="stylesheet" href="/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="/css/forked-daapd.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root" v-cloak>
|
||||
|
||||
|
||||
<!--
|
||||
############# Navbar #############
|
||||
-->
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">
|
||||
<b class="navbar-item">forked-daapd</b>
|
||||
<a class="navbar-item" href="https://github.com/ejurgensen/forked-daapd" title="GitHub"><i class="fa fa-github"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<!--
|
||||
############# Hero section #############
|
||||
-->
|
||||
<section class="hero is-dark is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
|
||||
<div class="column">
|
||||
<nav class="level is-mobile">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Artists</p>
|
||||
<p class="title">{{ library.artists }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Albums</p>
|
||||
<p class="title">{{ library.albums }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Songs</p>
|
||||
<p class="title">{{ library.songs }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Total playtime</p>
|
||||
<p class="title">{{ library.db_playtime | duration }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div> <!-- columns -->
|
||||
</div><!-- container -->
|
||||
</div><!-- hero -->
|
||||
</section>
|
||||
|
||||
|
||||
<!--
|
||||
############# Content section #############
|
||||
-->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
<span class="icon" v-show="library.updating"><i class="fa fa-refresh fa-spin"></i></span>
|
||||
<span class="icon" v-show="!library.updating"><i class="fa fa-refresh"></i></span>
|
||||
Update library
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
Scan new and modified items into your library.
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item is-primary" v-on:click="update" v-show="!library.updating">Update</a>
|
||||
<span class="card-footer-item" v-show="library.updating">Update in progress ...</span>
|
||||
</footer>
|
||||
</div> <!-- card update library -->
|
||||
</div> <!-- column -->
|
||||
|
||||
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
<span class="icon"><i class="fa fa-mobile"></i></span> Remote pairing
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content" v-show="pairing.active">
|
||||
<p>Remote pairing request from <b>{{pairing.remote}}</b></p>
|
||||
<form v-on:submit.prevent="kickoffPairing">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Enter pairing code" v-model="pairing_req.pin">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="content" v-show="!pairing.active">
|
||||
<p>No active pairing request.</p>
|
||||
<a class="button" v-on:click="loadPairing" v-show="!config.websocket_port">Refresh</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- card remote pairing -->
|
||||
</div> <!-- column -->
|
||||
|
||||
|
||||
<div class="column">
|
||||
<div class="card" v-show="spotify.enabled">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
<span class="icon"><i class="fa fa-spotify"></i></span> Spotify
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content" v-show="!spotify.libspotify_installed">
|
||||
<p><b>libspotify</b> is not installed (required for playing spotify tracks)</p>
|
||||
</div>
|
||||
<div class="content" v-show="spotify.libspotify_installed">
|
||||
<div v-show="!spotify.libspotify_logged_in"><p><b>libspotify</b> (requires Spotify premium account, enables playback of Spotify songs):</p>
|
||||
<form v-on:submit.prevent="loginLibspotify">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Username" v-model="libspotify.user">
|
||||
<p class="help is-danger">{{ libspotify.errors.user }}</p>
|
||||
</div>
|
||||
<div class="control">
|
||||
<input class="input" type="password" placeholder="Password" v-model="libspotify.password">
|
||||
<p class="help is-danger">{{ libspotify.errors.password }}</p>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button" type="submit">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger">{{ libspotify.errors.error }}</p>
|
||||
</form>
|
||||
</div>
|
||||
<p v-show="spotify.libspotify_logged_in"><b>libspotify</b> (requires Spotify premium account, enables playback of Spotify songs): logged in as <b>{{ spotify.libspotify_user }}</b></p>
|
||||
<hr>
|
||||
<div v-show="!spotify.webapi_token_valid">
|
||||
<p><b>Spotify Web API</b> access is required to add saved albums and playlists to your library.</p>
|
||||
<a class="button" v-bind:href="spotify.oauth_uri">Authorize Web API access</a>
|
||||
</div>
|
||||
<div v-show="spotify.webapi_token_valid">
|
||||
<p><b>Spotify Web API</b>: access authorized for <b>{{ spotify.webapi_user }}</b></p>
|
||||
<a class="button" v-bind:href="spotify.oauth_uri">Reauthorize Web API access</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- card spotify -->
|
||||
</div> <!-- column -->
|
||||
|
||||
</div> <!-- columns -->
|
||||
</div> <!-- container -->
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="content has-text-centered">
|
||||
<p>
|
||||
<strong>forked-daapd</strong> - version {{ config.version }}
|
||||
</p>
|
||||
<p class="is-size-7">Compiled with support for {{ config.buildoptions | join }}.</p>
|
||||
<p class="is-size-7">Web interface built with <a href="http://bulma.io">Bulma</a>, <a href="http://fontawesome.io/">Font Awesome</a>, <a href="https://vuejs.org/">Vue.js</a>, <a href="https://github.com/mzabriskie/axios">axios</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
</div> <!-- #root -->
|
||||
|
||||
<script src="/js/vue.min.js"></script>
|
||||
<script src="/js/axios.min.js"></script>
|
||||
<script src="/js/forked-daapd.js"></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
|
@ -0,0 +1,3 @@
|
|||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
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
|
@ -0,0 +1,111 @@
|
|||
|
||||
|
||||
var app = new Vue({
|
||||
el: '#root',
|
||||
data: {
|
||||
config: {},
|
||||
library: {},
|
||||
spotify: {},
|
||||
pairing: {},
|
||||
pairing_req: { pin: '' },
|
||||
libspotify: { user: '', password: '', errors: { user: '', password: '', error: '' } }
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.loadConfig();
|
||||
this.loadLibrary();
|
||||
this.loadSpotify();
|
||||
this.loadPairing();
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadConfig: function() {
|
||||
axios.get('/api/config').then(response => {
|
||||
this.config = response.data;
|
||||
this.connect()});
|
||||
},
|
||||
|
||||
loadLibrary: function() {
|
||||
axios.get('/api/library').then(response => this.library = response.data);
|
||||
},
|
||||
|
||||
loadSpotify: function() {
|
||||
axios.get('/api/spotify').then(response => this.spotify = response.data);
|
||||
},
|
||||
|
||||
loadPairing: function() {
|
||||
axios.get('/api/pairing').then(response => this.pairing = response.data);
|
||||
},
|
||||
|
||||
update: function() {
|
||||
this.library.updating = true;
|
||||
axios.get('/api/update').then(console.log('Library is updating'));
|
||||
},
|
||||
|
||||
kickoffPairing: function() {
|
||||
axios.post('/api/pairing', this.pairing_req).then(response => {
|
||||
console.log('Kicked off pairing');
|
||||
if (!this.config.websocket_port) {
|
||||
this.pairing = {};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loginLibspotify: function() {
|
||||
axios.post('/api/spotify-login', this.libspotify).then(response => {
|
||||
this.libspotify.user = '';
|
||||
this.libspotify.password = '';
|
||||
this.libspotify.errors.user = '';
|
||||
this.libspotify.errors.password = '';
|
||||
this.libspotify.errors.error = '';
|
||||
if (!response.data.success) {
|
||||
this.libspotify.errors.user = response.data.errors.user;
|
||||
this.libspotify.errors.password = response.data.errors.password;
|
||||
this.libspotify.errors.error = response.data.errors.error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
connect: function() {
|
||||
if (this.config.websocket_port <= 0) {
|
||||
console.log('Websocket disabled');
|
||||
return;
|
||||
}
|
||||
var socket = new WebSocket('ws://' + document.domain + ':' + this.config.websocket_port, 'notify');
|
||||
const vm = this;
|
||||
socket.onopen = function() {
|
||||
socket.send(JSON.stringify({ notify: ['update', 'pairing', 'spotify']}));
|
||||
socket.onmessage = function(response) {
|
||||
console.log(response.data); // upon message
|
||||
var data = JSON.parse(response.data);
|
||||
if (data.notify.includes('update')) {
|
||||
vm.loadLibrary();
|
||||
}
|
||||
if (data.notify.includes('pairing')) {
|
||||
vm.loadPairing();
|
||||
}
|
||||
if (data.notify.includes('spotify')) {
|
||||
vm.loadSpotify();
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
filters: {
|
||||
duration: function(seconds) {
|
||||
// Display seconds as hours:minutes:seconds
|
||||
|
||||
var h = Math.floor(seconds / 3600);
|
||||
var m = Math.floor(seconds % 3600 / 60);
|
||||
var s = Math.floor(seconds % 3600 % 60);
|
||||
|
||||
return h + ":" + ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2);
|
||||
},
|
||||
|
||||
join: function(array) {
|
||||
return array.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
})
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue