Merge pull request #1165 from chme/web_next

Update web interface v0.8.4
This commit is contained in:
Christian Meffert 2021-01-11 20:12:11 +01:00 committed by GitHub
commit 74c87f3080
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1478 additions and 1277 deletions

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

View File

@ -37,6 +37,7 @@ static struct settings_option webinterface_options[] =
{ "show_menu_item_radio", SETTINGS_TYPE_BOOL, { false } },
{ "show_menu_item_files", SETTINGS_TYPE_BOOL, { true } },
{ "show_menu_item_search", SETTINGS_TYPE_BOOL, { true } },
{ "recently_added_limit", SETTINGS_TYPE_INT, { 100 } },
};
static struct settings_option artwork_options[] =

2304
web-src/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "forked-daapd-web",
"version": "0.8.3",
"version": "0.8.4",
"private": true,
"description": "forked-daapd web interface",
"author": "chme <christian.meffert@googlemail.com>",
@ -14,39 +14,39 @@
"axios": "^0.21.1",
"bulma": "^0.9.1",
"bulma-switch": "^2.0.0",
"core-js": "^3.7.0",
"core-js": "^3.8.2",
"mdi": "^2.2.43",
"moment": "^2.29.1",
"moment-duration-format": "^2.3.2",
"npm": "^6.14.9",
"npm": "^6.14.11",
"reconnectingwebsocket": "^1.0.0",
"spotify-web-api-js": "^1.5.1",
"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-observe-visibility": "^1.0.0",
"vue-progressbar": "^0.7.5",
"vue-range-slider": "^0.6.0",
"vue-router": "^3.4.9",
"vue-scrollto": "^2.20.0",
"vue-tiny-lazyload-img": "^0.1.0",
"vuedraggable": "^2.24.3",
"vuex": "^3.5.1"
"vuex": "^3.6.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.9",
"@vue/cli-plugin-eslint": "^4.5.9",
"@vue/cli-service": "^4.5.9",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/cli-plugin-babel": "^4.5.10",
"@vue/cli-plugin-eslint": "^4.5.10",
"@vue/cli-service": "^4.5.10",
"@vue/eslint-config-standard": "^6.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.14.0",
"eslint": "^7.17.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^7.1.0",
"sass": "^1.29.0",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^7.4.1",
"sass": "^1.32.2",
"sass-loader": "^10.1.0",
"vue-template-compiler": "^2.6.12"
},

View File

@ -107,18 +107,18 @@ export default {
const vm = this
var protocol = 'ws://'
let protocol = 'ws://'
if (window.location.protocol === 'https:') {
protocol = 'wss://'
}
var wsUrl = protocol + window.location.hostname + ':' + vm.$store.state.config.websocket_port
let wsUrl = protocol + window.location.hostname + ':' + vm.$store.state.config.websocket_port
if (process.env.NODE_ENV === 'development' && process.env.VUE_APP_WEBSOCKET_SERVER) {
// If we are running in the development server, use the websocket url configured in .env.development
wsUrl = process.env.VUE_APP_WEBSOCKET_SERVER
}
var socket = new ReconnectingWebSocket(
const socket = new ReconnectingWebSocket(
wsUrl,
'notify',
{ reconnectInterval: 3000 }
@ -146,7 +146,7 @@ export default {
vm.$store.dispatch('add_notification', { text: 'Connection lost. Reconnecting ... (' + vm.reconnect_attempts + ')', type: 'danger', topic: 'connection' })
}
socket.onmessage = function (response) {
var data = JSON.parse(response.data)
const data = JSON.parse(response.data)
if (data.notify.includes('update') || data.notify.includes('database')) {
vm.update_library_stats()
}

View File

@ -10,7 +10,7 @@ export default {
// setup audio routing
setupAudio () {
var AudioContext = window.AudioContext || window.webkitAudioContext
const AudioContext = window.AudioContext || window.webkitAudioContext
this._context = new AudioContext()
this._source = this._context.createMediaElementSource(this._audio)
this._gain = this._context.createGain()

View File

@ -7,7 +7,7 @@
<span class="icon fd-has-action"
:class="{ 'has-text-grey-light': !output.selected }"
v-on:click="set_enabled">
<i class="mdi mdi-18px" :class="type_class"></i>
<i class="mdi mdi-18px" :class="type_class" :title="output.type"></i>
</span>
</a>
</div>
@ -42,7 +42,7 @@ export default {
computed: {
type_class () {
if (this.output.type === 'AirPlay') {
if (this.output.type.startsWith('AirPlay')) {
return 'mdi-airplay'
} else if (this.output.type === 'Chromecast') {
return 'mdi-cast'

View File

@ -0,0 +1,120 @@
<template>
<fieldset :disabled="disabled">
<div class="field">
<label class="label has-text-weight-normal">
<slot name="label"></slot>
<i class="is-size-7"
:class="{
'has-text-info': statusUpdate === 'success',
'has-text-danger': statusUpdate === 'error'
}"> {{ info }}</i>
</label>
<div class="control">
<input class="input"
type="number"
min="0"
style="width: 10em;"
:placeholder="placeholder"
:value="value"
@input="set_update_timer"
ref="settings_number">
</div>
<p class="help" v-if="$slots['info']">
<slot name="info"></slot>
</p>
</div>
</fieldset>
</template>
<script>
import webapi from '@/webapi'
import * as types from '@/store/mutation_types'
export default {
name: 'SettingsIntfield',
props: ['category_name', 'option_name', 'placeholder', 'disabled'],
data () {
return {
timerDelay: 2000,
timerId: -1,
statusUpdate: ''
}
},
computed: {
category () {
return this.$store.state.settings.categories.find(elem => elem.name === this.category_name)
},
option () {
if (!this.category) {
return {}
}
return this.category.options.find(elem => elem.name === this.option_name)
},
value () {
return this.option.value
},
info () {
if (this.statusUpdate === 'success') {
return '(setting saved)'
} else if (this.statusUpdate === 'error') {
return '(error saving setting)'
}
return ''
}
},
methods: {
set_update_timer () {
if (this.timerId > 0) {
window.clearTimeout(this.timerId)
this.timerId = -1
}
this.statusUpdate = ''
const newValue = this.$refs.settings_number.value
if (newValue !== this.value) {
this.timerId = window.setTimeout(this.update_setting, this.timerDelay)
}
},
update_setting () {
this.timerId = -1
const newValue = this.$refs.settings_number.value
if (newValue === this.value) {
this.statusUpdate = ''
return
}
const option = {
category: this.category.name,
name: this.option_name,
value: parseInt(newValue, 10)
}
webapi.settings_update(this.category.name, option).then(() => {
this.$store.commit(types.UPDATE_SETTINGS_OPTION, option)
this.statusUpdate = 'success'
}).catch(() => {
this.statusUpdate = 'error'
this.$refs.settings_number.value = this.value
}).finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
})
},
clear_status: function () {
this.statusUpdate = ''
}
}
}
</script>
<style>
</style>

View File

@ -19,6 +19,8 @@ export default class Albums {
getAlbumIndex (album) {
if (this.options.sort === 'Recently added') {
return album.time_added.substring(0, 4)
} else if (this.options.sort === 'Recently added (browse)') {
return this.getRecentlyAddedBrowseIndex(album.time_added)
} else if (this.options.sort === 'Recently released') {
return album.date_released ? album.date_released.substring(0, 4) : '0000'
} else if (this.options.sort === 'Release date') {
@ -27,6 +29,23 @@ export default class Albums {
return album.name_sort.charAt(0).toUpperCase()
}
getRecentlyAddedBrowseIndex (recentlyAdded) {
if (!recentlyAdded) {
return '0000'
}
const diff = new Date().getTime() - new Date(recentlyAdded).getTime()
if (diff < 86400000) { // 24h
return 'Today'
} else if (diff < 604800000) { // 7 days
return 'Last week'
} else if (diff < 2592000000) { // 30 days
return 'Last month'
}
return recentlyAdded.substring(0, 4)
}
isAlbumVisible (album) {
if (this.options.hideSingles && album.track_count <= 2) {
return false
@ -43,11 +62,11 @@ export default class Albums {
}
createSortedAndFilteredList () {
var albumsSorted = this.items
let 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') {
if (this.options.sort === 'Recently added' || this.options.sort === 'Recently added (browse)') {
albumsSorted = [...albumsSorted].sort((a, b) => b.time_added.localeCompare(a.time_added))
} else if (this.options.sort === 'Recently released') {
albumsSorted = [...albumsSorted].sort((a, b) => {

View File

@ -39,7 +39,7 @@ export default class Artists {
}
createSortedAndFilteredList () {
var artistsSorted = this.items
let artistsSorted = this.items
if (this.options.hideSingles || this.options.hideSpotify || this.options.hideOther) {
artistsSorted = artistsSorted.filter(artist => this.isArtistVisible(artist))
}

View File

@ -8,7 +8,7 @@
<p class="heading">albums</p>
</template>
<template slot="content">
<list-albums :albums="recently_added.items"></list-albums>
<list-albums :albums="albums_list"></list-albums>
</template>
</content-with-heading>
</div>
@ -20,13 +20,16 @@ import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsMusic from '@/components/TabsMusic'
import ListAlbums from '@/components/ListAlbums'
import webapi from '@/webapi'
import store from '@/store'
import Albums from '@/lib/Albums'
const browseData = {
load: function (to) {
const limit = store.getters.settings_option_recently_added_limit
return webapi.search({
type: 'album',
expression: 'media_kind is music having track_count > 3 order by time_added desc',
limit: 500
limit: limit
})
},
@ -42,7 +45,18 @@ export default {
data () {
return {
recently_added: {}
recently_added: { items: [] }
}
},
computed: {
albums_list () {
return new Albums(this.recently_added.items, {
hideSingles: false,
hideSpotify: false,
sort: 'Recently added (browse)',
group: true
})
}
}
}

View File

@ -135,7 +135,7 @@ export default {
methods: {
open_parent_directory: function () {
var parent = this.current_directory.slice(0, this.current_directory.lastIndexOf('/'))
const parent = this.current_directory.slice(0, this.current_directory.lastIndexOf('/'))
if (parent === '' || this.$store.state.config.directories.includes(this.current_directory)) {
this.$router.push({ path: '/files' })
} else {

View File

@ -124,9 +124,9 @@ export default {
},
move_item: function (e) {
var oldPosition = !this.show_only_next_items ? e.oldIndex : e.oldIndex + this.current_position
var item = this.queue_items[oldPosition]
var newPosition = item.position + (e.newIndex - e.oldIndex)
const oldPosition = !this.show_only_next_items ? e.oldIndex : e.oldIndex + this.current_position
const item = this.queue_items[oldPosition]
const newPosition = item.position + (e.newIndex - e.oldIndex)
if (newPosition !== oldPosition) {
webapi.queue_move(item.id, newPosition)
}

View File

@ -262,7 +262,7 @@ export default {
return
}
var searchParams = {
const searchParams = {
type: query.type,
media_kind: 'music'
}
@ -291,7 +291,7 @@ export default {
return
}
var searchParams = {
const searchParams = {
type: 'album',
media_kind: 'audiobook'
}
@ -317,7 +317,7 @@ export default {
return
}
var searchParams = {
const searchParams = {
type: 'album',
media_kind: 'podcast'
}

View File

@ -80,6 +80,18 @@
</settings-textfield>
</template>
</content-with-heading>
<content-with-heading>
<template slot="heading-left">
<div class="title is-4">Recently added page</div>
</template>
<template slot="content">
<settings-intfield category_name="webinterface" option_name="recently_added_limit">
<template slot="label">Limit the number of albums shown on the "Recently Added" page</template>
</settings-intfield>
</template>
</content-with-heading>
</div>
</template>
@ -88,10 +100,11 @@ import ContentWithHeading from '@/templates/ContentWithHeading'
import TabsSettings from '@/components/TabsSettings'
import SettingsCheckbox from '@/components/SettingsCheckbox'
import SettingsTextfield from '@/components/SettingsTextfield'
import SettingsIntfield from '@/components/SettingsIntfield'
export default {
name: 'SettingsPageWebinterface',
components: { ContentWithHeading, TabsSettings, SettingsCheckbox, SettingsTextfield },
components: { ContentWithHeading, TabsSettings, SettingsCheckbox, SettingsTextfield, SettingsIntfield },
computed: {
settings_option_show_composer_now_playing () {

View File

@ -278,10 +278,10 @@ export default {
return webapi.spotify().then(({ data }) => {
this.search_param.market = data.webapi_country
var spotifyApi = new SpotifyWebApi()
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(data.webapi_token)
var types = this.query.type.split(',').filter(type => this.validSearchTypes.includes(type))
const types = this.query.type.split(',').filter(type => this.validSearchTypes.includes(type))
return spotifyApi.search(this.query.query, types, this.search_param)
})
},

View File

@ -64,7 +64,7 @@ export default new Vuex.Store({
getters: {
now_playing: state => {
var item = state.queue.items.find(function (item) {
const item = state.queue.items.find(function (item) {
return item.id === state.player.item_id
})
return (item === undefined) ? {} : item
@ -77,6 +77,16 @@ export default new Vuex.Store({
return null
},
settings_option_recently_added_limit: (state, getters) => {
if (getters.settings_webinterface) {
const option = getters.settings_webinterface.options.find(elem => elem.name === 'recently_added_limit')
if (option) {
return option.value
}
}
return 100
},
settings_option_show_composer_now_playing: (state, getters) => {
if (getters.settings_webinterface) {
const option = getters.settings_webinterface.options.find(elem => elem.name === 'show_composer_now_playing')
@ -157,7 +167,7 @@ export default new Vuex.Store({
},
[types.ADD_NOTIFICATION] (state, notification) {
if (notification.topic) {
var index = state.notifications.list.findIndex(elem => elem.topic === notification.topic)
const index = state.notifications.list.findIndex(elem => elem.topic === notification.topic)
if (index >= 0) {
state.notifications.list.splice(index, 1, notification)
return
@ -173,7 +183,7 @@ export default new Vuex.Store({
}
},
[types.ADD_RECENT_SEARCH] (state, query) {
var index = state.recent_searches.findIndex(elem => elem === query)
const index = state.recent_searches.findIndex(elem => elem === query)
if (index >= 0) {
state.recent_searches.splice(index, 1)
}

View File

@ -63,7 +63,7 @@ export default {
},
queue_add_next (uri) {
var position = 0
let position = 0
if (store.getters.now_playing && store.getters.now_playing.id) {
position = store.getters.now_playing.position + 1
}
@ -74,7 +74,7 @@ export default {
},
queue_expression_add (expression) {
var options = {}
const options = {}
options.expression = expression
return axios.post('./api/queue/items/add', undefined, { params: options }).then((response) => {
@ -84,7 +84,7 @@ export default {
},
queue_expression_add_next (expression) {
var options = {}
const options = {}
options.expression = expression
options.position = 0
if (store.getters.now_playing && store.getters.now_playing.id) {
@ -109,7 +109,7 @@ export default {
},
player_play_uri (uris, shuffle, position = undefined) {
var options = {}
const options = {}
options.uris = uris
options.shuffle = shuffle ? 'true' : 'false'
options.clear = 'true'
@ -120,7 +120,7 @@ export default {
},
player_play_expression (expression, shuffle, position = undefined) {
var options = {}
const options = {}
options.expression = expression
options.shuffle = shuffle ? 'true' : 'false'
options.clear = 'true'
@ -159,12 +159,12 @@ export default {
},
player_shuffle (newState) {
var shuffle = newState ? 'true' : 'false'
const shuffle = newState ? 'true' : 'false'
return axios.put('./api/player/shuffle?state=' + shuffle)
},
player_consume (newState) {
var consume = newState ? 'true' : 'false'
const consume = newState ? 'true' : 'false'
return axios.put('./api/player/consume?state=' + consume)
},
@ -235,7 +235,7 @@ export default {
},
library_genre (genre) {
var genreParams = {
const genreParams = {
type: 'albums',
media_kind: 'music',
expression: 'genre is "' + genre + '"'
@ -246,7 +246,7 @@ export default {
},
library_genre_tracks (genre) {
var genreParams = {
const genreParams = {
type: 'tracks',
media_kind: 'music',
expression: 'genre is "' + genre + '"'
@ -257,7 +257,7 @@ export default {
},
library_radio_streams () {
var params = {
const params = {
type: 'tracks',
media_kind: 'music',
expression: 'data_kind is url and song_length = 0'
@ -269,7 +269,7 @@ export default {
library_artist_tracks (artist) {
if (artist) {
var artistParams = {
const artistParams = {
type: 'tracks',
expression: 'songartistid is "' + artist + '"'
}
@ -280,7 +280,7 @@ export default {
},
library_podcasts_new_episodes () {
var episodesParams = {
const episodesParams = {
type: 'tracks',
expression: 'media_kind is podcast and play_count = 0 ORDER BY time_added DESC'
}
@ -290,7 +290,7 @@ export default {
},
library_podcast_episodes (albumId) {
var episodesParams = {
const episodesParams = {
type: 'tracks',
expression: 'media_kind is podcast and songalbumid is "' + albumId + '" ORDER BY date_released DESC'
}
@ -336,7 +336,7 @@ export default {
},
library_files (directory = undefined) {
var filesParams = { directory: directory }
const filesParams = { directory: directory }
return axios.get('./api/library/files', {
params: filesParams
})

View File

@ -10,7 +10,7 @@ module.exports = {
assetsDir: 'player',
// Relative public path
// Relative public path
publicPath: './',
// Do not add hashes to the generated js/css filenames, would otherwise