mirror of
https://github.com/owntone/owntone-server.git
synced 2025-03-29 00:33:44 -04:00
[web-src] Add seek forward/backward buttons for podcasts/audiobooks
This commit is contained in:
parent
45e7816637
commit
bbacf3e406
@ -19,8 +19,10 @@
|
|||||||
|
|
||||||
<!-- Skip previous (not visible on "now playing" page) -->
|
<!-- Skip previous (not visible on "now playing" page) -->
|
||||||
<player-button-previous v-if="is_now_playing_page" class="navbar-item fd-margin-left-auto" icon_style="mdi-24px"></player-button-previous>
|
<player-button-previous v-if="is_now_playing_page" class="navbar-item fd-margin-left-auto" icon_style="mdi-24px"></player-button-previous>
|
||||||
|
<player-button-seek-back v-if="is_now_playing_page" seek_ms="10000" class="navbar-item" icon_style="mdi-24px"></player-button-seek-back>
|
||||||
<!-- Play/pause -->
|
<!-- Play/pause -->
|
||||||
<player-button-play-pause class="navbar-item" icon_style="mdi-36px" show_disabled_message></player-button-play-pause>
|
<player-button-play-pause class="navbar-item" icon_style="mdi-36px" show_disabled_message></player-button-play-pause>
|
||||||
|
<player-button-seek-forward v-if="is_now_playing_page" seek_ms="30000" class="navbar-item" icon_style="mdi-24px"></player-button-seek-forward>
|
||||||
<!-- Skip next (not visible on "now playing" page) -->
|
<!-- Skip next (not visible on "now playing" page) -->
|
||||||
<player-button-next v-if="is_now_playing_page" class="navbar-item" icon_style="mdi-24px"></player-button-next>
|
<player-button-next v-if="is_now_playing_page" class="navbar-item" icon_style="mdi-24px"></player-button-next>
|
||||||
|
|
||||||
@ -99,22 +101,11 @@
|
|||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<div class="level is-mobile fd-expanded">
|
<div class="level is-mobile fd-expanded">
|
||||||
<div class="level-left">
|
<div class="level-item">
|
||||||
<div class="level-item">
|
<div class="buttons has-addons">
|
||||||
<div class="buttons has-addons">
|
<player-button-repeat class="button"></player-button-repeat>
|
||||||
<player-button-previous class="button"></player-button-previous>
|
<player-button-shuffle class="button"></player-button-shuffle>
|
||||||
<player-button-play-pause class="button"></player-button-play-pause>
|
<player-button-consume class="button"></player-button-consume>
|
||||||
<player-button-next class="button"></player-button-next>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="level-right">
|
|
||||||
<div class="level-item">
|
|
||||||
<div class="buttons has-addons">
|
|
||||||
<player-button-repeat class="button"></player-button-repeat>
|
|
||||||
<player-button-shuffle class="button"></player-button-shuffle>
|
|
||||||
<player-button-consume class="button"></player-button-consume>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -166,47 +157,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Outputs dropdown menu
|
<!-- Outputs: speaker volumes -->
|
||||||
<div class="navbar-item has-dropdown"
|
<navbar-item-output v-for="output in outputs" :key="output.id" :output="output"></navbar-item-output>
|
||||||
:class="{ 'is-active': show_outputs_menu }">
|
|
||||||
<a class="navbar-link is-arrowless has-text-centered is-size-7" @click="show_outputs_menu = !show_outputs_menu">
|
|
||||||
<span class="icon"><i class="mdi mdi-18px" :class="{ 'mdi-chevron-up': !show_outputs_menu, 'mdi-chevron-down': show_outputs_menu }"></i></span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="navbar-dropdown is-right" v-show="show_outputs_menu">
|
<!-- Outputs: stream volume -->
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
-->
|
<div class="navbar-item">
|
||||||
<!-- Outputs: speaker volumes -->
|
<div class="level is-mobile">
|
||||||
<navbar-item-output v-for="output in outputs" :key="output.id" :output="output"></navbar-item-output>
|
<div class="level-left fd-expanded">
|
||||||
|
<div class="level-item" style="flex-grow: 0;">
|
||||||
<!-- Outputs: stream volume -->
|
<a class="button is-white is-small" :class="{ 'is-loading': loading }"><span class="icon fd-has-action" :class="{ 'has-text-grey-light': !playing && !loading, 'is-loading': loading }" @click="togglePlay"><i class="mdi mdi-18px mdi-radio-tower"></i></span></a>
|
||||||
<hr class="navbar-divider">
|
</div>
|
||||||
<div class="navbar-item">
|
<div class="level-item fd-expanded">
|
||||||
<div class="level is-mobile">
|
<div class="fd-expanded">
|
||||||
<div class="level-left 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>
|
||||||
<div class="level-item" style="flex-grow: 0;">
|
<range-slider
|
||||||
<a class="button is-white is-small" :class="{ 'is-loading': loading }"><span class="icon fd-has-action" :class="{ 'has-text-grey-light': !playing && !loading, 'is-loading': loading }" @click="togglePlay"><i class="mdi mdi-18px mdi-radio-tower"></i></span></a>
|
class="slider fd-has-action"
|
||||||
</div>
|
min="0"
|
||||||
<div class="level-item fd-expanded">
|
max="100"
|
||||||
<div class="fd-expanded">
|
step="1"
|
||||||
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="/stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p>
|
:disabled="!playing"
|
||||||
<range-slider
|
:value="stream_volume"
|
||||||
class="slider fd-has-action"
|
@change="set_stream_volume">
|
||||||
min="0"
|
</range-slider>
|
||||||
max="100"
|
|
||||||
step="1"
|
|
||||||
:disabled="!playing"
|
|
||||||
:value="stream_volume"
|
|
||||||
@change="set_stream_volume">
|
|
||||||
</range-slider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- </div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -223,12 +201,26 @@ import PlayerButtonPrevious from '@/components/PlayerButtonPrevious'
|
|||||||
import PlayerButtonShuffle from '@/components/PlayerButtonShuffle'
|
import PlayerButtonShuffle from '@/components/PlayerButtonShuffle'
|
||||||
import PlayerButtonConsume from '@/components/PlayerButtonConsume'
|
import PlayerButtonConsume from '@/components/PlayerButtonConsume'
|
||||||
import PlayerButtonRepeat from '@/components/PlayerButtonRepeat'
|
import PlayerButtonRepeat from '@/components/PlayerButtonRepeat'
|
||||||
|
import PlayerButtonSeekBack from '@/components/PlayerButtonSeekBack'
|
||||||
|
import PlayerButtonSeekForward from '@/components/PlayerButtonSeekForward'
|
||||||
import RangeSlider from 'vue-range-slider'
|
import RangeSlider from 'vue-range-slider'
|
||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NavbarBottom',
|
name: 'NavbarBottom',
|
||||||
components: { NavbarItemLink, NavbarItemOutput, RangeSlider, PlayerButtonPlayPause, PlayerButtonNext, PlayerButtonPrevious, PlayerButtonShuffle, PlayerButtonConsume, PlayerButtonRepeat },
|
components: {
|
||||||
|
NavbarItemLink,
|
||||||
|
NavbarItemOutput,
|
||||||
|
RangeSlider,
|
||||||
|
PlayerButtonPlayPause,
|
||||||
|
PlayerButtonNext,
|
||||||
|
PlayerButtonPrevious,
|
||||||
|
PlayerButtonShuffle,
|
||||||
|
PlayerButtonConsume,
|
||||||
|
PlayerButtonRepeat,
|
||||||
|
PlayerButtonSeekForward,
|
||||||
|
PlayerButtonSeekBack
|
||||||
|
},
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
38
web-src/src/components/PlayerButtonSeekBack.vue
Normal file
38
web-src/src/components/PlayerButtonSeekBack.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<a @click="seek" :disabled="disabled" v-if="visible">
|
||||||
|
<span class="icon"><i class="mdi mdi-rewind" :class="icon_style"></i></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PlayerButtonSeekBack',
|
||||||
|
props: ['seek_ms', 'icon_style'],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
now_playing () {
|
||||||
|
return this.$store.getters.now_playing
|
||||||
|
},
|
||||||
|
is_stopped () {
|
||||||
|
return this.$store.state.player.state === 'stop'
|
||||||
|
},
|
||||||
|
disabled () {
|
||||||
|
return !this.$store.state.queue || this.$store.state.queue.count <= 0 || this.is_stopped ||
|
||||||
|
this.now_playing.data_kind === 'pipe'
|
||||||
|
},
|
||||||
|
visible () {
|
||||||
|
return ['podcast', 'audiobook'].includes(this.now_playing.media_kind)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
seek: function () {
|
||||||
|
if (!this.disabled) {
|
||||||
|
webapi.player_seek(this.seek_ms * -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
38
web-src/src/components/PlayerButtonSeekForward.vue
Normal file
38
web-src/src/components/PlayerButtonSeekForward.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<a @click="seek" :disabled="disabled" v-if="visible">
|
||||||
|
<span class="icon"><i class="mdi mdi-fast-forward" :class="icon_style"></i></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PlayerButtonSeekForward',
|
||||||
|
props: ['seek_ms', 'icon_style'],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
now_playing () {
|
||||||
|
return this.$store.getters.now_playing
|
||||||
|
},
|
||||||
|
is_stopped () {
|
||||||
|
return this.$store.state.player.state === 'stop'
|
||||||
|
},
|
||||||
|
disabled () {
|
||||||
|
return !this.$store.state.queue || this.$store.state.queue.count <= 0 || this.is_stopped ||
|
||||||
|
this.now_playing.data_kind === 'pipe'
|
||||||
|
},
|
||||||
|
visible () {
|
||||||
|
return ['podcast', 'audiobook'].includes(this.now_playing.media_kind)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
seek: function () {
|
||||||
|
if (!this.disabled) {
|
||||||
|
webapi.player_seek(this.seek_ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,36 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a v-on:click="play_skip_back">
|
|
||||||
<i v-if="is_skip_allowed">
|
|
||||||
<span class="icon"><i class="mdi mdi-replay"></i></span>
|
|
||||||
</i>
|
|
||||||
<i v-else>
|
|
||||||
<span class="icon has-text-grey-light"><i class="mdi mdi-replay"></i></span>
|
|
||||||
</i>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import webapi from '@/webapi'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'PlayerButtonSkipBack',
|
|
||||||
props: [ 'when_ms' ],
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
is_skip_allowed () {
|
|
||||||
return this.$store.state.player.state !== 'stop' && this.$store.getters.now_playing && this.$store.getters.now_playing.data_kind !== 'url' && this.$store.getters.now_playing.data_kind !== 'pipe'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
play_skip_back: function () {
|
|
||||||
if (this.is_skip_allowed) {
|
|
||||||
webapi.player_seek(this.when_ms - 10000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
@ -1,36 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a v-on:click="play_skip_fwd">
|
|
||||||
<i v-if="is_skip_allowed">
|
|
||||||
<span class="icon"><i class="mdi mdi-flip-h mdi-replay"></i></span>
|
|
||||||
</i>
|
|
||||||
<i v-else>
|
|
||||||
<span class="icon has-text-grey-light"><i class="mdi mdi-flip-h mdi-replay"></i></span>
|
|
||||||
</i>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import webapi from '@/webapi'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'PlayerButtonSkipFwd',
|
|
||||||
props: [ 'when_ms' ],
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
is_skip_allowed () {
|
|
||||||
return this.$store.state.player.state !== 'stop' && this.$store.getters.now_playing && this.$store.getters.now_playing.data_kind !== 'url' && this.$store.getters.now_playing.data_kind !== 'pipe'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
play_skip_fwd: function () {
|
|
||||||
if (this.is_skip_allowed) {
|
|
||||||
webapi.player_seek(this.when_ms + 10000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
@ -176,7 +176,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
seek: function (newPosition) {
|
seek: function (newPosition) {
|
||||||
webapi.player_seek(newPosition).catch(() => {
|
webapi.player_seek_to_pos(newPosition).catch(() => {
|
||||||
this.item_progress_ms = this.state.item_progress_ms
|
this.item_progress_ms = this.state.item_progress_ms
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -178,10 +178,14 @@ export default {
|
|||||||
return axios.put('/api/player/volume?volume=' + outputVolume + '&output_id=' + outputId)
|
return axios.put('/api/player/volume?volume=' + outputVolume + '&output_id=' + outputId)
|
||||||
},
|
},
|
||||||
|
|
||||||
player_seek (newPosition) {
|
player_seek_to_pos (newPosition) {
|
||||||
return axios.put('/api/player/seek?position_ms=' + newPosition)
|
return axios.put('/api/player/seek?position_ms=' + newPosition)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
player_seek (seekMs) {
|
||||||
|
return axios.put('/api/player/seek?seek_ms=' + seekMs)
|
||||||
|
},
|
||||||
|
|
||||||
outputs () {
|
outputs () {
|
||||||
return axios.get('/api/outputs')
|
return axios.get('/api/outputs')
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user