Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • petre/nextcloud-app-podcast
  • onny/nextcloud-app-podcast
2 results
Show changes
Showing
with 816 additions and 706 deletions
......@@ -21,21 +21,15 @@
-->
<template>
<div class="podcastSection">
<div class="podcastSectionHeader">
<h1>{{ title }}</h1>
</div>
<div class="grid">
<div
v-for="(podcast, idx) in podcasts"
:key="idx"
class="podcastCard">
<router-link :to="{ path: `/browse/show/${podcast.id}`}">
<div v-show="podcast.smallImageURL"
<div
v-lazy:background-image="podcast.smallImageURL"
class="podcastImage" />
<div v-show="podcast.imgurl"
v-lazy:background-image="podcast.imgurl"
class="podcastImage" />
<span class="title">
{{ podcast.title }}
</span>
......@@ -49,16 +43,13 @@
</template>
<script>
export default {
name: 'ItemGrid',
props: {
title: {
type: String,
default: '',
},
podcasts: {
type: Object,
default() { return {} },
type: Array,
default() { return [] },
},
},
}
......@@ -70,22 +61,6 @@ export default {
margin-bottom: 20px;
}
.podcastSectionHeader {
padding: 10px 0px;
z-index: 60;
position: sticky;
top: 50px;
background: white;
display: flex;
align-items: center;
margin-bottom: 10px;
h1 {
flex-grow: 1;
font-size: 1.6em;
}
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
......@@ -103,6 +78,10 @@ export default {
cursor: pointer;
}
&:hover {
background: rgb(236, 236, 236);
}
.podcastImage {
background-size: cover;
background-position: center;
......@@ -120,17 +99,17 @@ export default {
text-overflow: ellipsis;
white-space: nowrap;
display: block;
&.title {
font-size: 1em;
}
&.subtitle {
font-size: 0.9em;
color: #b5b1b1;
}
}
span.title {
font-size: 1em;
}
span.subtitle {
font-size: 0.9em;
color: #b5b1b1;
}
}
.podcastCard:hover {
background: rgb(236, 236, 236);
}
</style>
......@@ -20,18 +20,15 @@
-
-->
<template>
<div class="podcastSection">
<div class="podcastSectionHeader">
<h1>{{ title }}</h1>
<a :href="showallurl">Show all</a>
</div>
<div v-show="podcasts.length"
class="podcastSection">
<div class="podcastSliderWrapper">
<div
v-show="showPrev"
class="navSlider navPrev"
@click="moveSlider('left')" />
<div
id="slider"
ref="slider"
class="podcastSlider"
:class="[showPrev ? '' : 'hideBefore', showNext ? '' : 'hideAfter']">
<div
......@@ -63,14 +60,6 @@
export default {
name: 'ItemSlider',
props: {
title: {
type: String,
default: '',
},
showallurl: {
type: String,
default: '',
},
podcasts: {
type: Array,
default() { return [] },
......@@ -80,8 +69,17 @@ export default {
showPrev: false,
showNext: true,
}),
watch: {
podcasts(newValue, oldValue) {
const slider = this.$refs.slider
console.log(slider.scrollLeft, slider.scrollWidth, slider.clientWidth)
if (slider.scrollWidth > slider.clientWidth) {
this.showNext = true
}
},
},
mounted() {
const slider = this.$el.querySelector('#slider')
const slider = this.$refs.slider
const vm = this
slider.addEventListener('scroll', function() {
if (slider.scrollLeft > 0) {
......@@ -99,7 +97,7 @@ export default {
},
methods: {
moveSlider(direction) {
const slider = this.$el.querySelector('#slider')
const slider = this.$refs.slider
const sliderPos = slider.scrollLeft
if (direction === 'right') {
slider.scrollLeft = sliderPos + 350
......@@ -116,26 +114,7 @@ export default {
.podcastSection {
margin-bottom: 20px;
}
.podcastSectionHeader {
padding: 10px 0px;
z-index: 60;
position: sticky;
top: 0px;
background: white;
display: flex;
align-items: center;
margin-bottom: 10px;
h1 {
flex-grow: 1;
font-size: 1.6em;
}
a {
color: #1976d2;
cursor: pointer;
}
padding: 0 30px;
}
.podcastSliderWrapper {
......@@ -170,18 +149,18 @@ export default {
width: 100px;
background-image: linear-gradient(to left, white 0%, rgba(255,255,255,0) 100%);
}
}
.podcastSlider::-webkit-scrollbar {
display: none;
}
&::-webkit-scrollbar {
display: none;
}
.podcastSlider.hideBefore:before {
display: none;
}
&.hideBefore:before {
display: none;
}
.podcastSlider.hideAfter:after {
display: none;
&.hideAfter:after {
display: none;
}
}
.navSlider {
......@@ -195,14 +174,14 @@ export default {
background-repeat: no-repeat;
background-position: center center;
background-size: 80%;
}
.navSlider:hover {
background: rgb(236, 236, 236);
background-image: var(--icon-triangle-e-000);
background-repeat: no-repeat;
background-position: center center;
background-size: 80%;
&:hover {
background: rgb(236, 236, 236);
background-image: var(--icon-triangle-e-000);
background-repeat: no-repeat;
background-position: center center;
background-size: 80%;
}
}
.navNext {
......@@ -248,17 +227,19 @@ export default {
white-space: nowrap;
display: block;
}
span.title {
&.title {
font-size: 1em;
}
span.subtitle {
&.subtitle {
font-size: 0.9em;
color: #b5b1b1;
}
}
.podcastCard:hover {
background: rgb(236, 236, 236);
&:hover {
background: rgb(236, 236, 236);
}
}
</style>
<!--
- @copyright Copyright (c) 2021 Jonas Heinrich
-
- @author Jonas Heinrich <onny@project-insanity.org>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div v-resize:debounce="onResize">
<slot />
<EmptyContent
v-show="page !== null"
icon="icon-loading"
class="tableLoading" />
</div>
</template>
<script>
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
import resize from 'vue-resize-directive'
export default {
name: 'LoadMore',
components: {
EmptyContent,
},
directives: {
resize,
},
props: {
page: {
type: Number,
default: 0,
},
},
mounted() {
document.getElementById('app-content-vue').addEventListener('scroll', this.handleScroll)
},
destroyed() {
document.getElementById('app-content-vue').removeEventListener('scroll', this.handleScroll)
},
methods: {
preFill() {
if (this.page === null) {
document.getElementById('app-content-vue').removeEventListener('scroll', this.handleScroll)
} else {
const yFill = document.getElementsByClassName('tableLoading')[0].getBoundingClientRect().bottom
const playerPosY = document.getElementsByClassName('player')[0].getBoundingClientRect().top
if (yFill > 0 && yFill < playerPosY) {
this.$emit('load-more')
}
}
},
/**
* On scroll event, load more shows if bottom reached
*/
handleScroll() {
if (this.page === null) {
document.getElementById('app-content-vue').removeEventListener('scroll', this.handleScroll)
} else {
const appContent = document.getElementById('app-content-vue')
if (appContent.scrollTop === (appContent.scrollHeight - appContent.clientHeight)) {
this.$emit('load-more')
}
}
},
onResize() {
this.preFill()
},
},
}
</script>
<style lang="scss">
.empty-content.tableLoading {
margin-top: 10px !important;
.empty-content__icon {
margin-bottom: 0px !important;
}
}
</style>
......@@ -28,13 +28,13 @@
<div class="podcastDescription">
<h1>{{ title }}</h1>
<div class="podcastAuthor">
by <a :href="htmlurl" :target="isshow ? '_blank' : ''">{{ author }}</a>
{{ t('podcast', 'by') }} <a :href="htmlurl" :target="isshow ? '_blank' : ''">{{ author }}</a>
</div>
<div
v-show="isshow"
class="podcastControls">
<button class="icon-add-white podcastButton button new-button"
:class="showExists(podcastid) ? 'icon-delete' : 'icon-add-white primary'"
:class="dateadded ? 'icon-delete' : 'icon-add-white primary'"
@click="doSubscribe()">
{{ getSubscribeText }}
</button>
......@@ -50,6 +50,8 @@
</div>
<vue-show-more-text
:text="description"
:more-text="t('podcast', 'Show more')"
:less-text="t('podcast', 'Show less')"
additional-container-css="padding: 0px;" />
<slot />
</div>
......@@ -60,7 +62,6 @@
<script>
import vueShowMoreText from 'vue-show-more-text'
import { ShowApi } from './../services/ShowApi'
import { mapGetters } from 'vuex'
const apiClient = new ShowApi()
......@@ -104,13 +105,14 @@ export default {
type: Boolean,
default: false,
},
dateadded: {
type: Number,
default: null,
},
},
computed: {
...mapGetters([
'showExists',
]),
getSubscribeText() {
if (this.showExists(this.podcastid)) {
if (this.dateadded) {
return t('podcast', 'Unsubscribe')
} else {
return t('podcast', 'Subscribe')
......@@ -119,7 +121,7 @@ export default {
},
methods: {
doSubscribe() {
this.$emit('doSubscribe')
this.$emit('do-subscribe')
},
getCategoryName(categoryid) {
return apiClient.getCategoryName(categoryid)
......@@ -199,23 +201,26 @@ export default {
}
}
ul.podcastCategory li {
display: inline-block;
border: 1px solid var(--color-text-maxcontrast);
border-radius: var(--border-radius);
margin-right: 5px;
cursor: pointer;
color: var(--color-text-maxcontrast);
padding: 0px 7px;
margin-bottom: 5px;
}
ul.podcastCategory {
li {
display: inline-block;
border: 1px solid var(--color-text-maxcontrast);
border-radius: var(--border-radius);
margin-right: 5px;
cursor: pointer;
color: var(--color-text-maxcontrast);
padding: 0px 7px;
margin-bottom: 5px;
&:hover {
border: 1px solid white;
color: white;
}
}
ul.podcastCategory li:hover {
border: 1px solid white;
color: white;
}
}
}
.podcastButton {
......
......@@ -66,31 +66,31 @@ export default {
float: left;
top: 5px;
margin-right: 5px;
}
.eq-animation .-amp-video-eq-col {
flex: 1;
position: relative;
height: 100%;
margin-right: 1px;
}
.-amp-video-eq-col {
flex: 1;
position: relative;
height: 100%;
margin-right: 1px;
.eq-animation .-amp-video-eq-col div {
animation-name: amp-video-eq-animation;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
background-color: rgb(0, 130, 201);
position: absolute;
width: 100%;
height: 100%;
transform: translateY(100%);
will-change: transform;
}
div {
animation-name: amp-video-eq-animation;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
background-color: rgb(0, 130, 201);
position: absolute;
width: 100%;
height: 100%;
transform: translateY(100%);
will-change: transform;
}
}
.eq-animation.paused .-amp-video-eq-col div {
animation-name: unset;
transform: translateY(65%);
&.paused .-amp-video-eq-col div {
animation-name: unset;
transform: translateY(65%);
}
}
.-amp-video-eq-1-1 {
......
......@@ -25,7 +25,6 @@
<div class="playerControls">
<button
class="primary icon-play-previous-white"
style="background-position: 45% 50%;"
@click="seekEpisode(currentSeek - 10)" />
<div
class="wrap"
......@@ -33,45 +32,53 @@
<button
class="primary big"
:class="isPlaying ? 'icon-pause-white' : 'icon-play-white'"
@click="togglePlay" />
@click="doTogglePlay" />
</div>
<button
class="primary icon-play-next-white"
style="background-position: 55% 50%;"
@click="seekEpisode(currentSeek + 10)" />
</div>
<div class="metaControls">
<div
v-lazy:background-image="getImage"
class="playerThumb" />
<span class="title">{{ getTitle }}</span>
v-lazy:background-image="getEpisode.imgURL"
class="playerThumb"
@click="$router.push(`/browse/show/${getEpisode.podcast_id}/${getEpisode.id}`)" />
<div class="text">
<span class="title">
<a :href="`#/browse/show/${getEpisode.podcast_id}/${getEpisode.id}`">{{ getEpisode.title }}</a>
</span>
<span>
<a :href="`#/browse/show/${getEpisode.podcast_id}`">{{ getPodcastName }}</a>
</span>
</div>
</div>
<div class="playbackControls">
<span>{{ secondsToHHMMSS(currentSeek) }} / {{ secondsToHHMMSS(getDuration) }}</span>
<span>{{ secondsToHHMMSS(currentSeek) }} / {{ secondsToHHMMSS( getEpisode.duration) }}</span>
<input
type="range"
class="rangeStyle"
min="0"
:max="getDuration"
:max="getEpisode.duration"
step=".5"
:value="currentSeek"
:disabled="!getSeek"
@input="changeTempSeek($event.target.value)"
@input="restyleInput(); changeTempSeek($event.target.value)"
@change="seekLocked = false; seekEpisode($event.target.value)">
</div>
<div class="volumeControls">
<div
class="volumeIcon"
:class="getVolume == 0 ? 'volumeMute' : 'volumeFull'"
@click="toggleMute" />
@click="toggleMute(); restyleInput()" />
<input
class="volume"
class="volume rangeStyle"
type="range"
name="volume"
min="0"
max="1"
step=".05"
:value="getVolume"
@input="setVolume($event.target.value)">
@input="restyleInput(); setVolume($event.target.value)">
</div>
</div>
</template>
......@@ -82,19 +89,19 @@ import { mapGetters, mapActions } from 'vuex'
export default {
name: 'Player',
data: () => ({
tmpSeek: null,
tmpSeek: 0,
seekLocked: false,
}),
computed: {
...mapGetters([
'getVolume',
'getSeek',
'getTitle',
'getImage',
'getDuration',
'isBuffering',
'isPlaying',
'isPaused',
'getEpisode',
'getPodcastName',
'getPaused',
]),
currentSeek() {
if (this.seekLocked) {
......@@ -104,6 +111,17 @@ export default {
}
},
},
watch: {
currentSeek() {
const vm = this
setTimeout(function() {
vm.restyleInput()
}, 1000)
},
},
mounted() {
this.restyleInput()
},
methods: {
...mapActions([
'setVolume',
......@@ -116,11 +134,26 @@ export default {
this.seekLocked = true
this.tmpSeek = value
},
secondsToHHMMSS(duration) {
let hhmmss = new Date(duration * 1000).toISOString().substr(11, 8)
secondsToHHMMSS(seconds) {
if (!seconds) {
seconds = 0
}
let hhmmss = new Date(seconds * 1000).toISOString().substr(11, 8)
hhmmss = hhmmss.replace(/^(00:)/, '')
return hhmmss
},
restyleInput() {
const element0 = document.getElementsByClassName('rangeStyle')[0]
let value = (element0.value - element0.min) / (element0.max - element0.min) * 100
element0.style.background = 'linear-gradient(to right, #fff 0%, #fff ' + value + '%, #70bbe4 ' + value + '%, #70bbe4 100%)'
const element1 = document.getElementsByClassName('rangeStyle')[1]
value = (element1.value - element1.min) / (element1.max - element1.min) * 100
element1.style.background = 'linear-gradient(to right, #fff 0%, #fff ' + value + '%, #70bbe4 ' + value + '%, #70bbe4 100%)'
},
doTogglePlay() {
this.togglePlay()
this.seekEpisode(this.currentSeek)
},
},
}
</script>
......@@ -142,29 +175,41 @@ export default {
justify-content: space-between;
padding: 0 20px;
width: 100%;
gap: 20px;
// gap: 20px; FIXME NOT YET SUPPORTED IN CHROME
}
.player > * {
margin-right: 20px;
}
.playerControls {
display: flex;
align-items: center;
justify-content: space-between;
}
.playerControls button {
min-width: 34px;
min-height: 34px;
}
button {
min-width: 34px;
min-height: 34px;
.playerControls button.big {
background-position: 57% 50%;
min-width: 40px;
min-height: 40px;
background-size: 50%;
}
&.big {
background-position: 57% 50%;
min-width: 40px;
min-height: 40px;
background-size: 50%;
}
&.big.icon-pause-white {
background-position: 50% 50%;
}
.playerControls button.big.icon-pause-white {
background-position: 50% 50%;
&.icon-play-previous-white {
background-position: 45% 50%;
}
&.icon-play-next-white {
background-position: 55% 50%;
}
}
}
.wrap {
......@@ -196,24 +241,43 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
// gap: 20px; FIXME NOT YET SUPPORTED IN CHROME
.playerThumb {
width: 50px;
height: 50px;
background-size: cover;
background-position: center;
margin-right: 20px;
cursor: pointer;
}
.metaControls .title {
color: white;
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: bold;
}
.text {
display: flex;
flex-direction: column;
color: white;
span {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.title {
font-weight: bold;
margin-bottom: -5px;
}
a {
color: white;
&:hover {
text-decoration: underline;
}
}
}
}
.playerThumb {
width: 50px;
height: 50px;
background: #ddd;
background-size: cover;
background-position: center;
}
.playbackControls {
......@@ -221,6 +285,7 @@ export default {
align-items: center;
justify-content: space-between;
flex-grow: 1;
max-width: 700px;
span {
color: white;
......@@ -232,67 +297,45 @@ export default {
width: 100%;
}
input[type=range] {
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
width: 100%; /* Specific width is required for Firefox. */
background: transparent; /* Otherwise white in Chrome */
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
}
input[type=range]:focus {
outline: none; /* Removes the blue border. You should probably do some kind of focus styling for accessibility reasons though. */
}
}
input[type=range]::-ms-track {
width: 100%;
height: 8.4px;
cursor: pointer;
background: transparent;
border-color: transparent;
border-width: 16px 0;
color: transparent;
input[type=range] {
-webkit-appearance: none;
width: 100%;
height: 5px;
min-height: 5px;
background: linear-gradient(to right, #fff 0%, #fff 0%, #70bbe4 0%, #70bbe4 100%);
border-radius: 3px;
outline: none;
&:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 5px;
cursor: pointer;
background: #3071a9;
border-radius: 3px;
&::-moz-range-progress {
background: white;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #367ebd;
&::-moz-range-track {
background: #70bbe4;
}
input[type=range]::-moz-range-track {
width: 100%;
height: 5px;
cursor: pointer;
background: #3071a9;
border-radius: 3px;
&::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
height: 15px;
width: 15px;
border-radius: 50%;
background: white;
}
input[type=range]::-ms-fill-lower {
background: #2a6495;
border: 0.2px solid #010101;
border-radius: 2.6px;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
}
input[type=range]:focus::-ms-fill-lower {
background: #3071a9;
}
input[type=range]::-ms-fill-upper {
background: #3071a9;
border: 0.2px solid #010101;
border-radius: 2.6px;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
}
input[type=range]:focus::-ms-fill-upper {
background: #367ebd;
&::-moz-range-thumb {
-webkit-appearance: none;
border: none;
height: 15px;
width: 15px;
border-radius: 50%;
background: white;
}
}
......@@ -301,27 +344,47 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
}
.volumeIcon {
width: 25px;
height: 25px;
cursor: pointer;
margin-right: 10px;
.volumeIcon {
width: 30px;
height: 25px;
cursor: pointer;
margin-right: 10px;
&.volumeFull {
background-color: white;
mask-repeat: no-repeat;
mask-size: 100%;
mask-image: var(--icon-sound-000);
}
&.volumeMute {
background-color: white;
mask-repeat: no-repeat;
mask-size: 100%;
mask-image: var(--icon-sound-off-000);
}
}
}
.volumeFull {
background-color: white;
mask-repeat: no-repeat;
mask-size: 100%;
mask-image: var(--icon-sound-000);
@media only screen and (max-width: 1000px) {
.volumeControls {
display: none;
}
.player {
padding-right: 10px;
}
}
.volumeMute {
background-color: white;
mask-repeat: no-repeat;
mask-size: 100%;
mask-image: var(--icon-sound-off-000);
@media only screen and (max-width: 800px) {
.metaControls {
.text {
display: none;
}
.playerThumb {
margin-right: 0px;
}
}
}
</style>
......@@ -73,14 +73,14 @@
icon="icon-info"
:close-after-click="true"
@click="changeRoute(`/browse/show/${podcastid(episode)}/${episode.id}`)">
{{ t('podcast', 'Show episode') }}
{{ t('podcast', 'Go to episode') }}
</ActionButton>
<ActionButton
v-show="extended"
icon="icon-info"
:close-after-click="true"
@click="changeRoute(`/browse/show/${podcastid(episode)}`)">
{{ t('podcast', 'Show podcast') }}
{{ t('podcast', 'Go to show') }}
</ActionButton>
<ActionButton
icon="icon-download"
......@@ -186,6 +186,7 @@ export default {
escapedEpisodeDescription(episodeDescription) {
episodeDescription = episodeDescription
.replaceAll(/\n/g, ' ')
.replaceAll('<br/>', ' ')
.replaceAll('<br />', ' ')
.replaceAll('<br>', ' ')
.replaceAll('<ul>', ' ')
......@@ -205,7 +206,7 @@ export default {
window.open(episodeURL, '_blank')
},
doPlay(episode) {
this.$emit('doPlay', episode)
this.$emit('do-play', episode)
this.seekEpisode(episode.playtime)
},
changeRoute(path) {
......@@ -252,27 +253,27 @@ table.episodeTable {
padding: 15px;
height: 50px;
color: var(--color-text-maxcontrast);
}
th.iconColumn {
padding: 0px;
width: 115px;
}
&.iconColumn {
padding: 0px;
width: 115px;
}
th.nameColumn {
width: 100%;
}
&.nameColumn {
width: 100%;
}
th.actionColumn {
width: 72px;
}
&.actionColumn {
width: 72px;
}
th.durationColumn {
width: 90px;
}
&.durationColumn {
width: 90px;
}
th.dateColumn {
width: 130px;
&.dateColumn {
width: 130px;
}
}
}
......@@ -285,6 +286,7 @@ table.episodeTable {
transition: opacity 500ms ease 0s;
.selected {
// FIXME: Does this apply?
background: var(--color-primary-light);
}
......@@ -292,58 +294,60 @@ table.episodeTable {
cursor: pointer;
}
&:hover td {
background: var(--color-background-hover);
}
}
td {
padding: 0 15px;
font-style: normal;
border-bottom: 1px solid var(--color-border);
}
td.iconColumn {
padding-right: 0px;
padding-left: 35px;
.episodeImage {
width: 74px;
height: 74px;
background: #ccc;
background-size: cover;
background-position: center;
transition: opacity .4s ease;
border-radius: 5px;
}
&.iconColumn {
padding-right: 0px;
padding-left: 35px;
.episodeImage:hover div {
background: rgba(0, 0, 0, 0.6);
width: 100%;
height: 100%;
border-radius: 5px;
background-position: center;
background-image: var(--icon-play-fff);
background-size: 40%;
background-repeat: no-repeat;
}
.episodeImage {
width: 74px;
height: 74px;
background: #ccc;
background-size: cover;
background-position: center;
transition: opacity .4s ease;
border-radius: 5px;
}
.episodeImage:hover div.pause {
background-image: var(--icon-pause-fff);
}
}
.episodeImage:hover div {
background: rgba(0, 0, 0, 0.6);
width: 100%;
height: 100%;
border-radius: 5px;
background-position: center;
background-image: var(--icon-play-fff);
background-size: 40%;
background-repeat: no-repeat;
}
td.nameColumn {
overflow: hidden;
text-overflow: ellipsis;
padding-right: 0px;
b {
color: var(--color-main-text);
user-select: none;
cursor: pointer;
font-size: 1.05em;
.episodeImage:hover div.pause {
background-image: var(--icon-pause-fff);
}
}
}
tr:hover td {
background: var(--color-background-hover);
&.nameColumn {
overflow: hidden;
text-overflow: ellipsis;
padding-right: 0px;
b {
color: var(--color-main-text);
user-select: none;
cursor: pointer;
font-size: 1.05em;
}
}
}
}
......@@ -352,35 +356,39 @@ table.episodeTable {
@media only screen and (max-width: 500px) {
table {
thead {
th.iconColumn {
width: 85px;
}
th.nameColumn {
padding-left: 0px;
}
th.actionColumn {
padding-left: 5px;
padding-right: 5px;
width: 50px;
}
th.durationColumn, th.dateColumn {
display: none;
th {
&.iconColumn {
width: 85px;
}
&.nameColumn {
padding-left: 0px;
}
&.actionColumn {
padding-left: 5px;
padding-right: 5px;
width: 50px;
}
&.durationColumn, &.dateColumn {
display: none;
}
}
}
tbody {
td.iconColumn {
padding-left: 10px;
}
td.nameColumn {
padding-left: 0px;
}
td.actionColumn {
padding-left: 5px;
padding-right: 5px;
width: 50px;
}
td.durationColumn, td.dateColumn {
display: none;
td {
&.iconColumn {
padding-left: 10px;
}
&.nameColumn {
padding-left: 0px;
}
&.actionColumn {
padding-left: 5px;
padding-right: 5px;
width: 50px;
}
&.durationColumn, &.dateColumn {
display: none;
}
}
}
}
......
......@@ -28,10 +28,8 @@ import { translate, translatePlural } from '@nextcloud/l10n'
import App from './App'
import VueResizeObserver from 'vue-resize-observer'
import VueLazyload from 'vue-lazyload'
Vue.use(VueResizeObserver)
Vue.use(VueLazyload)
Vue.prototype.t = translate
......@@ -39,7 +37,7 @@ Vue.prototype.n = translatePlural
Vue.prototype.OC = window.OC
Vue.prototype.OCA = window.OCA
Vue.prototype.$apiUrl = 'https://api.fyyd.de/0.2'
Vue.prototype.$version = '0.0.1'
Vue.prototype.$version = '0.3.1'
export default new Vue({
el: '#content',
......
......@@ -23,8 +23,6 @@
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
const requesttoken = axios.defaults.headers.requesttoken
export class EpisodeApi {
url(url) {
......@@ -35,17 +33,16 @@ export class EpisodeApi {
addEpisode(episode) {
episode = {
id: episode.id,
imgurl: episode.imgURL,
imgURL: episode.imgURL,
title: episode.title,
pubdate: episode.pubdate,
duration: episode.duration,
playtime: 0,
lastplayed: Date.now(),
enclosure: episode.enclosure,
description: episode.description,
podcastid: episode.podcast_id,
description: episode.description.substring(0, 100), // Trim string description, because of MySQL max length field
podcast_id: episode.podcast_id,
}
axios.defaults.headers.requesttoken = requesttoken
return axios.post(this.url('/episodes'), episode)
.then(
(response) => {
......@@ -61,7 +58,6 @@ export class EpisodeApi {
}
removeEpisode(episode) {
axios.defaults.headers.requesttoken = requesttoken
return axios.delete(this.url(`/episodes/${episode.id}`))
.then(
(response) => {
......@@ -76,9 +72,43 @@ export class EpisodeApi {
})
}
loadEpisodes(episode) {
axios.defaults.headers.requesttoken = requesttoken
return axios.get(this.url('/episodes'))
queryEpisodes(podcastId = null, page = 0, sortBy = 'lastplayed') {
let params = {}
if (podcastId) {
params = {
podcast_id: podcastId,
count: 20,
page,
}
} else {
params = {
count: 20,
page,
sortBy,
}
}
return axios.get(this.url('/episodes'), {
params,
})
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
queryEpisode(episodeId) {
return axios.get(this.url('/episodes'), {
params: {
episode_id: episodeId,
},
})
.then(
(response) => {
return Promise.resolve(response.data)
......@@ -90,22 +120,22 @@ export class EpisodeApi {
.catch((err) => {
return Promise.reject(err)
})
}
updateEpisode({ episode, playtime } = {}) {
episode = {
id: episode.id,
imgurl: episode.imgURL || episode.imgurl,
imgURL: episode.imgURL,
title: episode.title,
pubdate: episode.pubdate,
duration: episode.duration,
playtime,
lastplayed: Date.now(),
enclosure: episode.enclosure,
description: episode.description,
podcastid: episode.podcast_id || episode.podcastid,
description: episode.description.substring(0, 100), // Trim string description, because of MySQL max length field
podcast_id: episode.podcast_id,
}
axios.defaults.headers.requesttoken = requesttoken
return axios.put(this.url(`/episodes/${episode.id}`), episode)
.then(
(response) => {
......
/*
* @copyright Copyright (c) 2021 Jonas Heinrich <onny@project-insanity.org>
*
* @author Jonas Heinrich <onny@project-insanity.org>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import axios from '@nextcloud/axios'
export class FyydApi {
url() {
const url = 'https://api.fyyd.de/0.2'
return url
}
queryPodcast(podcastId) {
delete axios.defaults.headers.requesttoken
return axios.get(this.url() + '/podcast', {
params: {
podcast_id: podcastId,
},
})
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
queryEpisodes(podcastId, page) {
delete axios.defaults.headers.requesttoken
return axios.get(this.url() + '/podcast/episodes', {
params: {
podcast_id: podcastId,
count: 20,
page,
},
})
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
queryEpisode(episodeId) {
delete axios.defaults.headers.requesttoken
return axios.get(this.url() + '/episode', {
params: {
episode_id: episodeId,
},
})
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
queryList(listName, count = 10) {
delete axios.defaults.headers.requesttoken
let queryURL = ''
if (listName === 'hot') {
queryURL = this.url() + '/feature/podcast/hot'
} else if (listName === 'latest') {
queryURL = this.url() + '/podcast/latest'
} else {
queryURL = this.url() + '/category?category_id=' + listName
}
return axios.get(queryURL, {
params: {
count,
},
})
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
}
......@@ -21,14 +21,13 @@
*/
import { Howl, Howler } from 'howler'
import { showError } from '@nextcloud/dialogs'
import store from './../store/main.js'
let audioPlayer = null
export class Player {
doPlay(src) {
load(src) {
if (audioPlayer !== null) {
audioPlayer.fade(store.getters.getVolume, 0, 500)
......@@ -41,46 +40,38 @@ export class Player {
src,
html5: true,
volume: store.getters.getVolume,
onLoad() {
store.dispatch('setBuffering', true)
},
onplay() {
const duration = audioPlayer.duration()
store.dispatch('setPlaying', true)
store.dispatch('setBuffering', false)
store.dispatch('setDuration', duration)
store.dispatch('setPausing', false)
},
onpause() {
store.dispatch('setPlaying', false)
store.dispatch('setPausing', true)
store.dispatch('setBuffering', false)
},
onend() {
showError(t('podcast', 'Lost connection to podcast station, retrying ...'))
store.dispatch('setPlaying', false)
store.dispatch('setBuffering', true)
},
})
audioPlayer.unload()
audioPlayer.play()
audioPlayer.fade(0, store.getters.getVolume, 500)
}
updateSeek() {
const vm = this
setTimeout(function() {
if (audioPlayer !== null) {
const seek = audioPlayer.seek()
store.dispatch('setSeek', seek)
store.dispatch('storePlaybacktime', seek)
vm.updateSeek()
}
}, 1000)
play() {
audioPlayer.play()
audioPlayer.fade(0, store.getters.getVolume, 500)
}
doPause() {
pause() {
audioPlayer.pause()
}
doResume() {
resume() {
audioPlayer.play()
}
......@@ -88,10 +79,32 @@ export class Player {
audioPlayer.seek(startTime)
}
setVolume(volume) {
volume(volume) {
if (audioPlayer !== null) {
audioPlayer.volume(volume)
}
}
state() {
return audioPlayer
}
getSeek() {
return audioPlayer.seek()
}
updateSeek() {
const vm = this
setTimeout(function() {
if (audioPlayer !== null) {
if (audioPlayer.state() === 'loaded') {
const seek = audioPlayer.seek()
store.dispatch('setSeek', seek)
store.dispatch('storePlaybacktime', seek)
}
vm.updateSeek()
}
}, 1000)
}
}
......@@ -23,7 +23,6 @@
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
const requesttoken = axios.defaults.headers.requesttoken
const categories = require('../assets/categories.json')
export class ShowApi {
......@@ -36,13 +35,14 @@ export class ShowApi {
addShow(show) {
show = {
id: show.id,
imgurl: show.smallImageURL,
smallImageURL: show.smallImageURL,
title: show.title,
author: show.author,
lastpub: show.lastpub,
dateadded: Date.now(),
homepage: show.htmlURL,
feedurl: show.xmlURL,
}
axios.defaults.headers.requesttoken = requesttoken
return axios.post(this.url('/shows'), show)
.then(
(response) => {
......@@ -58,7 +58,6 @@ export class ShowApi {
}
removeShow(show) {
axios.defaults.headers.requesttoken = requesttoken
return axios.delete(this.url(`/shows/${show.id}`))
.then(
(response) => {
......@@ -73,9 +72,35 @@ export class ShowApi {
})
}
loadShows(show) {
axios.defaults.headers.requesttoken = requesttoken
return axios.get(this.url('/shows'))
queryShow(podcastId) {
return axios.get(this.url('/shows'), {
params: {
podcast_id: podcastId,
},
})
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
queryShows(page) {
return axios.get(this.url('/shows'), {
params: {
count: 20,
page,
},
})
.then(
(response) => {
return Promise.resolve(response.data)
......@@ -87,6 +112,30 @@ export class ShowApi {
.catch((err) => {
return Promise.reject(err)
})
}
queryCategory(category, count = 20, page = 0) {
return axios.get(this.url('/shows'), {
params: {
category: String(category),
count,
page,
},
})
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
getCategoryName(categoryid) {
......
......@@ -22,7 +22,7 @@
import { EpisodeApi } from './../services/EpisodeApi'
const apiClient = new EpisodeApi()
const episodeApiClient = new EpisodeApi()
export default {
state: {
......@@ -35,15 +35,8 @@ export default {
episodeById: state => (id) => {
return state.episodes.find((episode) => episode.id === id)
},
episodeExists: state => (id) => {
return state.episodes.some((episode) => episode.id === id)
},
},
mutations: {
addEpisode(state, episode) {
state.episodes.unshift(episode)
},
removeEpisode(state, episode) {
const existingIndex = state.episodes.findIndex(_episode => _episode.id === episode.id)
if (existingIndex !== -1) {
......@@ -55,35 +48,48 @@ export default {
},
updateEpisode(state, { episode, playtime } = {}) {
const existingIndex = state.episodes.findIndex(_episode => _episode.id === episode.id)
state.episodes[existingIndex].playtime = playtime
state.episodes[existingIndex].lastplayed = Date.now()
if (existingIndex >= 0) {
state.episodes[existingIndex].playtime = playtime
state.episodes[existingIndex].lastplayed = Date.now()
}
},
},
actions: {
async loadEpisodes({ commit }) {
const episodes = await apiClient.loadEpisodes()
commit('setEpisodes', episodes)
async loadEpisodes(context) {
const episodes = await episodeApiClient.queryEpisodes(null, 0)
if (episodes.data.episodes.length) {
context.dispatch('loadEpisode', episodes.data.episodes[0])
}
},
addEpisode({ commit, getters }, episode) {
if (getters.episodeExists(episode.id)) {
return true
}
apiClient.addEpisode(episode)
.then((episode) => {
commit('addEpisode', episode)
})
episode.lastplayed = Date.now()
episodeApiClient.addEpisode(episode)
},
removeEpisode({ commit }, episode) {
apiClient.removeEpisode(episode)
episodeApiClient.removeEpisode(episode)
.then((episode) => {
commit('removeEpisode', episode)
})
},
updateEpisode({ commit, getters }, { episode, playtime } = {}) {
apiClient.updateEpisode({ episode, playtime })
episodeApiClient.updateEpisode({ episode, playtime })
.then((episode) => {
commit('updateEpisode', { episode, playtime })
})
},
async queryEpisodes({ commit, getters }, { page, sortBy, podcastId } = {}) {
const response = await episodeApiClient.queryEpisodes(podcastId, page, sortBy)
const episodes = getters.getEpisodes.concat(response.data.episodes)
commit('setEpisodes', episodes)
if (response.meta.paging.next_page === null) {
return false
} else {
return true
}
},
clearEpisodes({ commit }) {
commit('setEpisodes', [])
},
},
}
......@@ -21,7 +21,7 @@
*/
import Vue from 'vue'
import Vuex from 'vuex'
import Vuex, { Store } from 'vuex'
import show from './show'
import player from './player'
......@@ -29,7 +29,7 @@ import episode from './episode'
Vue.use(Vuex)
export default new Vuex.Store({
export default new Store({
modules: {
show,
player,
......
......@@ -21,7 +21,10 @@
*/
import { Player } from './../services/Player'
import { ShowApi } from './../services/ShowApi'
const player = new Player()
const showApiClient = new ShowApi()
export default {
state: {
......@@ -29,35 +32,33 @@ export default {
isBuffering: false,
isMute: false,
isPaused: false,
volume: parseFloat(localStorage.getItem('podcast.volume')),
volume: 0.5,
oldVolume: null,
title: '',
image: '',
episodeId: null,
duration: null,
seek: null,
seek: 0,
lastplaytimeupdate: null,
podcastName: '',
episode: {
id: null,
podcast_id: null,
title: null,
imgURL: null,
playtime: null,
duration: null,
enclosure: null,
lastplayed: null,
},
},
getters: {
getTitle: state => {
return state.title
},
getImage: state => {
return state.image
},
getSeek: state => {
return state.seek
},
getDuration: state => {
return state.duration
},
getVolume: state => {
if (state.volume) {
return state.volume
if (state.seek) {
return state.seek
} else {
return 0.5
return 0
}
},
getVolume: state => {
return state.volume
},
isPlaying: state => {
return state.isPlaying
},
......@@ -65,20 +66,26 @@ export default {
return state.isBuffering
},
isPaused: state => (id) => {
return (state.episodeId === id && state.isPaused)
return (state.episode.id === id && state.isPaused)
},
episodeLoaded: state => (id) => {
return (state.episodeId === id)
return (state.episode.id === id)
},
episodePlaying: state => (id) => {
return (state.episodeId === id && state.isPlaying)
return (state.episode.id === id && state.isPlaying)
},
getEpisode: state => {
return state.episode
},
getPodcastName: state => {
return state.podcastName
},
},
mutations: {
setVolume(state, volume) {
state.volume = volume
localStorage.setItem('podcast.volume', volume)
player.setVolume(state.volume)
localStorage.setItem('podcast.volume', state.volume)
player.volume(state.volume)
},
toggleMute(state) {
if (state.isMute) {
......@@ -94,18 +101,13 @@ export default {
}
}
localStorage.setItem('podcast.volume', state.volume)
player.setVolume(state.volume)
},
setTitle(state, title) {
state.title = title
},
setDuration(state, duration) {
duration = Math.round(duration)
state.duration = duration
player.volume(state.volume)
},
setSeek(state, seek) {
seek = Math.round(seek)
state.seek = seek
if (seek) {
state.seek = seek
}
},
seekEpisode(state, seek) {
player.seek(seek)
......@@ -121,11 +123,11 @@ export default {
setBuffering(state, bufferingState) {
state.isBuffering = bufferingState
},
setImage(state, image) {
state.image = image
setPodcastName(state, name) {
state.podcastName = name
},
setEpisodeId(state, episodeId) {
state.episodeId = episodeId
setEpisode(state, episode) {
state.episode = episode
},
},
actions: {
......@@ -138,34 +140,37 @@ export default {
setVolume(context, volume) {
context.commit('setVolume', volume)
},
loadVolume(context) {
const volume = localStorage.getItem('podcast.volume')
if (typeof volume === 'string') {
context.commit('setVolume', volume)
}
},
toggleMute(context) {
context.commit('toggleMute')
},
loadEpisode(context, episode) {
context.commit('setEpisode', episode)
context.commit('setPausing', true)
context.dispatch('setPodcastName', episode.podcast_id)
if (episode.playtime) {
context.commit('setSeek', episode.playtime)
}
player.load(episode.enclosure)
},
playEpisode(context, episode) {
if (context.state.isPaused && episode.id === context.state.episodeId) {
context.commit('setPausing', false)
context.commit('setBuffering', true)
player.doResume()
if (context.state.isPaused && episode.id === context.state.episode.id) { // FIXME: use getter: episodeIsLoaded
player.play()
} else {
context.commit('setBuffering', true)
context.commit('setTitle', episode.title)
if (episode.imgURL) {
context.commit('setImage', episode.imgURL)
} else {
context.commit('setImage', episode.imgurl)
context.dispatch('loadEpisode', episode)
if (!episode.lastplayed) {
context.dispatch('addEpisode', episode)
}
context.commit('setEpisodeId', episode.id)
context.commit('setPausing', false)
context.commit('setPlaying', true)
context.dispatch('addEpisode', episode)
player.doPlay(episode.enclosure)
player.play()
}
},
pauseEpisode(context) {
context.commit('setBuffering', false)
context.commit('setPlaying', false)
context.commit('setPausing', true)
player.doPause()
player.pause()
},
setSeek(context, startTime) {
......@@ -173,38 +178,38 @@ export default {
},
togglePlay(context) {
if (context.state.episodeId === null) {
if (context.state.episode.id === null) {
return true
}
if (context.state.isPaused) {
context.commit('setPlaying', true)
context.commit('setBuffering', true)
context.commit('setPausing', false)
player.doResume()
player.play()
} else {
context.commit('setBuffering', false)
context.commit('setPausing', true)
context.commit('setPausing', true)
player.doPause()
player.pause()
}
},
setDuration(context, duration) {
context.commit('setDuration', duration)
seekEpisode(context, startTime) {
if (Math.abs(startTime - player.getSeek() > 1)) {
context.commit('seekEpisode', startTime)
}
},
seekEpisode(context, startTime) {
context.commit('seekEpisode', startTime)
setPausing(context, state) {
context.commit('setPausing', state)
},
storePlaybacktime(context, position) {
const episodeId = context.state.episodeId
const episode = context.getters.episodeById(episodeId)
const episodeLastPlayed = episode.lastplayed
const episode = context.state.episode
const dateNow = Date.now()
if (dateNow - episodeLastPlayed >= 5000) {
if (dateNow - episode.lastplayed >= 5000) {
context.dispatch('updateEpisode', { episode, playtime: position })
context.state.episode.lastplayed = Date.now()
}
},
async setPodcastName(context, podcastId) {
const podcast = await showApiClient.queryShow(podcastId)
context.commit('setPodcastName', podcast.data.title)
},
},
}
......@@ -22,25 +22,16 @@
import { ShowApi } from './../services/ShowApi'
const apiClient = new ShowApi()
const showApiClient = new ShowApi()
export default {
state: {
shows: [],
},
getters: {
subscribedShows: state => {
return state.shows
},
showById: state => (id) => {
return state.shows.find((show) => show.id === id)
},
showExists: state => (id) => {
if (id !== undefined) {
id = Number(id)
return state.shows.some((show) => show.id === id)
}
},
},
mutations: {
addShow(state, show) {
......@@ -58,17 +49,17 @@ export default {
},
actions: {
async loadShows({ commit }) {
const shows = await apiClient.loadShows()
commit('setShows', shows)
const shows = await showApiClient.queryShows()
commit('setShows', shows.data)
},
addShow({ commit }, show) {
apiClient.addShow(show)
showApiClient.addShow(show)
.then((show) => {
commit('addShow', show)
})
},
removeShow({ commit }, show) {
apiClient.removeShow(show)
showApiClient.removeShow(show)
.then((show) => {
commit('removeShow', show)
})
......
/**
* Helpfer function to update custom browser title.
*
* @param {string} title Title part to use.
*/
export function setBrowserTitle(title) {
document.title = title + ' - Podcast - Nextcloud'
}
......@@ -20,18 +20,19 @@
-
-->
<template>
<div>
<div style="padding-top: 10px;">
<BrowseEmpty v-show="loading" />
<div
v-show="!loading"
class="mainContent">
v-show="!loading">
<Header :title="t('podcast', 'Hot podcasts')">
<a href="#/browse/hot">Show all</a>
</Header>
<ItemSlider
title="Hot podcasts"
showallurl="#/browse/hot"
:podcasts="podcastsHot" />
<Header :title="t('podcast', 'New podcasts')">
<a href="#/browse/new">Show all</a>
</Header>
<ItemSlider
title="New podcasts"
showallurl="#/browse/new"
:podcasts="podcastsLatest" />
</div>
</div>
......@@ -40,15 +41,17 @@
<script>
import BrowseEmpty from './placeholder/Browse'
import ItemSlider from '../components/ItemSlider'
import Header from '../components/Header'
import { setBrowserTitle } from '../utils/misc.js'
import { FyydApi } from './../services/FyydApi'
const fyydClient = new FyydApi()
import { ShowApi } from './../services/ShowApi'
const showApiClient = new ShowApi()
export default {
name: 'Browse',
components: {
BrowseEmpty,
ItemSlider,
Header,
},
data: () => ({
podcastsHot: [],
......@@ -57,15 +60,15 @@ export default {
}),
mounted() {
this.queryPodcastLists()
setBrowserTitle('Browse')
setBrowserTitle(t('podcast', 'Browse'))
},
methods: {
async queryPodcastLists() {
const hotList = await fyydClient.queryList('hot', 10)
const hotList = await showApiClient.queryCategory('hot', 10)
this.podcastsHot = hotList.data
const latestList = await fyydClient.queryList('latest', 10)
const latestList = await showApiClient.queryCategory('latest', 10)
this.podcastsLatest = latestList.data
this.loading = false
......@@ -74,9 +77,3 @@ export default {
},
}
</script>
<style lang="scss">
.mainContent {
padding: 20px 30px;
}
</style>
......@@ -20,63 +20,73 @@
-
-->
<template>
<div class="mainContent">
<ItemGrid
:title="title"
:podcasts="podcasts" />
<div>
<Header :title="title" />
<LoadMore :page="page"
@load-more="queryPodcasts(page)">
<ItemGrid
:podcasts="podcasts" />
</LoadMore>
</div>
</template>
<script>
import ItemGrid from '../components/ItemGrid'
import Header from '../components/Header'
import LoadMore from '../components/LoadMore'
import { setBrowserTitle } from '../utils/misc.js'
import { FyydApi } from './../services/FyydApi'
const fyydClient = new FyydApi()
import { ShowApi } from './../services/ShowApi'
const showApiClient = new ShowApi()
export default {
name: 'BrowseAll',
components: {
ItemGrid,
Header,
LoadMore,
},
data: () => ({
podcasts: {},
podcasts: [],
category: null,
categoryId: null,
title: '',
page: 0,
}),
mounted() {
this.category = this.$route.params.category
this.categoryId = this.$route.params.categoryId
this.queryPodcasts()
},
methods: {
async queryPodcasts() {
async queryPodcasts(page = 0) {
let podcasts = null
if (this.category === 'hot') {
podcasts = await fyydClient.queryList('hot', 20)
this.title = 'Hot podcasts'
this.podcasts = podcasts.data
podcasts = await showApiClient.queryCategory('hot', 20, page)
this.title = t('podcast', 'Hot podcasts')
this.podcasts = this.podcasts.concat(podcasts.data)
} else if (this.category === 'new') {
podcasts = await fyydClient.queryList('latest', 20)
this.title = 'New podcasts'
this.podcasts = podcasts.data
podcasts = await showApiClient.queryCategory('latest', 20, page)
this.title = t('podcast', 'New podcasts')
this.podcasts = this.podcasts.concat(podcasts.data)
} else if (this.category === 'subscriptions') {
podcasts = await showApiClient.queryShows(page)
this.title = t('podcast', 'My subscriptions')
this.podcasts = this.podcasts.concat(podcasts.data)
} else {
podcasts = await showApiClient.queryCategory(this.categoryId, 20, page)
this.title = t('podcast', 'Podcasts in') + ' ' + podcasts.data.category.title
this.podcasts = this.podcasts.concat(podcasts.data.podcasts)
}
if (podcasts.meta.paging.next_page === null) {
this.page = null
} else {
podcasts = await fyydClient.queryList(this.categoryId, 20)
this.title = 'Podcasts in ' + podcasts.data.category.title
this.podcasts = podcasts.data.podcasts
this.page += 1
}
setBrowserTitle(this.title)
},
},
}
</script>
<style lang="scss">
.mainContent {
padding: 30px;
}
</style>
......@@ -38,11 +38,11 @@
</button>
<div class="episodeDetails">
<span>
<b>Duration:</b>
<b>{{ t('podcast', 'Duration') }}:</b>
{{ episode.duration_string }}
</span>
<span>
<b>Publication date:</b>
<b>{{ t('podcast', 'Publication date') }}:</b>
<span class="inline" :title="readableDate(episode.pubdate)">
{{ readableTimeAgo(episode.pubdate) }}
</span>
......@@ -50,7 +50,7 @@
</div>
</MediaHeader>
<ContentCollapsable
title="Episode notes">
:title="t('podcast', 'Episode description')">
<!-- eslint-disable -->
<p
v-html="episodeDescriptionParsed"
......@@ -59,7 +59,7 @@
</ContentCollapsable>
<ContentCollapsable
v-show="episode.chapters"
title="Episode chapters">
:title="t('podcast', 'Episode chapters')">
<table class="chapterTable">
<tbody>
<tr
......@@ -93,12 +93,12 @@ import MediaHeader from '../components/MediaHeader'
import { mapGetters, mapActions } from 'vuex'
import TimeAgo from 'javascript-time-ago'
import PlayAnimation from '../components/PlayAnimation'
import { FyydApi } from './../services/FyydApi'
import { setBrowserTitle } from '../utils/misc.js'
const fyydClient = new FyydApi()
import { EpisodeApi } from './../services/EpisodeApi'
import { ShowApi } from './../services/ShowApi'
const episodeApiClient = new EpisodeApi()
const showApiClient = new ShowApi()
const timeAgo = new TimeAgo('en-US')
......@@ -242,7 +242,7 @@ export default {
},
async queryEpisode(episodeId) {
const episode = await fyydClient.queryEpisode(episodeId)
const episode = await episodeApiClient.queryEpisode(episodeId)
this.processEpisode(episode)
},
......@@ -253,7 +253,7 @@ export default {
},
async queryPodcastName(podcastId) {
const podcast = await fyydClient.queryPodcast(podcastId)
const podcast = await showApiClient.queryShow(podcastId)
this.podcastName = podcast.data.title
},
......@@ -291,35 +291,35 @@ table.chapterTable {
tbody {
td {
padding: 15px 0px;
padding-left: 30px;
font-style: normal;
border-bottom: 1px solid var(--color-border);
cursor: pointer;
}
tr {
height: 30px;
background-color: var(--color-background-light);
transition: opacity 500ms ease 0s;
}
tr td * {
cursor: pointer;
}
td {
padding: 15px 0px;
padding-left: 30px;
font-style: normal;
border-bottom: 1px solid var(--color-border);
cursor: pointer;
td.timeColumn {
width: 120px;
color: #1976d2;
}
* {
cursor: pointer;
}
td.titleColumn {
padding-left: 0px;
}
&.timeColumn {
width: 120px;
color: #1976d2;
}
tr:hover, tr:focus, tr.mouseOver td {
background-color: var(--color-background-hover);
&.titleColumn {
padding-left: 0px;
}
}
&:hover, &:focus, &.mouseOver td {
background-color: var(--color-background-hover);
}
}
}
......