diff --git a/src/router.js b/src/router.js index d0e03fb63c61ff3ce8565a8cb0b5a22e0de9ec37..0d368b30baaa6ce683daa2e1e1f58e3d23a61d39 100644 --- a/src/router.js +++ b/src/router.js @@ -29,9 +29,12 @@ import Main from './components/Main' import Show from './views/Show' import Episode from './views/Episode' import Browse from './views/Browse' +import BrowseAll from './views/BrowseAll' import NotImplemented from './views/NotImplemented' import store from './store.js' +import BrowseEmpty from './views/placeholder/Browse' + Vue.use(Router) const requesttoken = axios.defaults.headers.requesttoken @@ -41,7 +44,7 @@ const router = new Router({ routes: [ { path: '/listening', - component: NotImplemented, + component: BrowseEmpty, name: 'LISTENING', }, { @@ -54,6 +57,11 @@ const router = new Router({ component: Browse, name: 'BROWSE', }, + { + path: '/browse/hot', + component: BrowseAll, + name: 'BROWSE', + }, { path: '/browse/show/:id', component: Show, diff --git a/src/views/Browse.vue b/src/views/Browse.vue index 1758159259fcd8a6a29c70f306ccb9253d8df534..b82afbc0647b05c46ff36f1c119ce958d5e22bf4 100644 --- a/src/views/Browse.vue +++ b/src/views/Browse.vue @@ -20,45 +20,73 @@ - --> <template> - <div class="mainContent"> - <div class="podcastSection"> - <div class="podcastSectionHeader"> - <h1>Hot podcasts</h1> - <a href="">Show all</a> - </div> - <div class="podcastSlider"> - <div class="navPrev" /> - <div class="navNext" /> - <div - v-for="(podcast, idx) in podcastsHot" - :key="idx" - class="podcastCard"> - <router-link :to="{ path: `/browse/show/${podcast.id}`}"> - <div - class="podcastImage" - :style="{ backgroundImage: `url(${podcast.smallImageURL})` }" /> - <span class="title"> - {{ podcast.title }} - </span> - <span class="subtitle"> - {{ podcast.author }} - </span> - </router-link> + <div> + <BrowseEmpty v-show="pageLoading" /> + <div + v-show="!pageLoading" + class="mainContent"> + <div class="podcastSection"> + <div class="podcastSectionHeader"> + <h1>Hot podcasts</h1> + <a href="#/browse/hot">Show all</a> + </div> + <div class="podcastSlider"> + <div class="navPrev" /> + <div class="navNext" /> + <div + v-for="(podcast, idx) in podcastsHot" + :key="idx" + class="podcastCard"> + <router-link :to="{ path: `/browse/show/${podcast.id}`}"> + <div + class="podcastImage" + :style="{ backgroundImage: `url(${podcast.smallImageURL})` }" /> + <span class="title"> + {{ podcast.title }} + </span> + <span class="subtitle"> + {{ podcast.author }} + </span> + </router-link> + </div> </div> </div> - </div> - <div class="podcastSection"> - <div class="podcastSectionHeader"> - <h1>New podcasts</h1> - <a href="">Show all</a> + <div class="podcastSection"> + <div class="podcastSectionHeader"> + <h1>New podcasts</h1> + <a href="#/browse/new">Show all</a> + </div> + <div class="podcastSlider"> + <div + v-for="(podcast, idx) in podcastsLatest" + :key="idx" + class="podcastCard"> + <router-link :to="{ path: `/browse/show/${podcast.id}`}"> + <div + class="podcastImage" + :style="{ backgroundImage: `url(${podcast.smallImageURL})` }" /> + <span class="title"> + {{ podcast.title }} + </span> + <span class="subtitle"> + {{ podcast.author }} + </span> + </router-link> + </div> + </div> </div> - <div class="podcastSlider"> - <div - v-for="(podcast, idx) in podcastsLatest" - :key="idx" - class="podcastCard"> - <router-link :to="{ path: `/browse/show/${podcast.id}`}"> + + <div class="podcastSection"> + <div class="podcastSectionHeader"> + <h1>Podcast charts</h1> + <a href="">Show all</a> + </div> + <div class="podcastSlider"> + <div + v-for="(podcast, idx) in podcastsHot" + :key="idx" + class="podcastCard"> <div class="podcastImage" :style="{ backgroundImage: `url(${podcast.smallImageURL})` }" /> @@ -68,30 +96,7 @@ <span class="subtitle"> {{ podcast.author }} </span> - </router-link> - </div> - </div> - </div> - - <div class="podcastSection"> - <div class="podcastSectionHeader"> - <h1>Podcast charts</h1> - <a href="">Show all</a> - </div> - <div class="podcastSlider"> - <div - v-for="(podcast, idx) in podcastsHot" - :key="idx" - class="podcastCard"> - <div - class="podcastImage" - :style="{ backgroundImage: `url(${podcast.smallImageURL})` }" /> - <span class="title"> - {{ podcast.title }} - </span> - <span class="subtitle"> - {{ podcast.author }} - </span> + </div> </div> </div> </div> @@ -99,14 +104,20 @@ </template> <script> +import BrowseEmpty from './placeholder/Browse' + import { showError } from '@nextcloud/dialogs' import axios from '@nextcloud/axios' export default { name: 'Browse', + components: { + BrowseEmpty, + }, data: () => ({ podcastsHot: {}, podcastsLatest: {}, + pageLoading: true, }), mounted() { this.queryTopPodcasts() @@ -134,6 +145,7 @@ export default { await axios.get(queryURI) .then(function(response) { vm.podcastsLatest = response.data.data + vm.pageLoading = false }) } catch (error) { showError(t('podcast', 'Could not fetch podcasts from remote API')) @@ -146,7 +158,7 @@ export default { <style lang="scss"> .mainContent { - padding: 30px; + padding: 20px 30px; h1 { font-size: 1.6em; @@ -155,7 +167,7 @@ export default { } .podcastSection { - margin-bottom: 30px; + margin-bottom: 20px; } .podcastSectionHeader { diff --git a/src/views/Episode.vue b/src/views/Episode.vue index fc8d914c9ebc78da2b4218a66b0eb24bc0135d38..d3843c21f04de9d8328cdcedac0a723331628424 100644 --- a/src/views/Episode.vue +++ b/src/views/Episode.vue @@ -21,7 +21,7 @@ --> <template> <div> - <ShowEmpty v-show="pageLoading" /> + <EpisodeEmpty v-show="pageLoading" /> <transition name="fade"> <div v-show="!pageLoading"> <MediaHeader @@ -77,7 +77,7 @@ import { showError } from '@nextcloud/dialogs' import axios from '@nextcloud/axios' -import ShowEmpty from './ShowEmpty' +import EpisodeEmpty from './placeholder/Episode' import ContentCollapsable from '../components/ContentCollapsable' import MediaHeader from '../components/MediaHeader' @@ -88,16 +88,15 @@ const timeAgo = new TimeAgo('en-US') export default { name: 'Episode', components: { - ShowEmpty, + EpisodeEmpty, ContentCollapsable, MediaHeader, }, data: () => ({ episode: {}, - pageLoading: false, + pageLoading: true, }), mounted() { - this.pageLoading = true const episodeId = this.$route.params.episodeId this.queryEpisode(episodeId) }, diff --git a/src/views/Show.vue b/src/views/Show.vue index 857ebe4fc6652cb29dac395ddf7455715e71ad1e..a9106671ea3475cb609bcbd671508976df07b12a 100644 --- a/src/views/Show.vue +++ b/src/views/Show.vue @@ -21,9 +21,9 @@ --> <template> <div> - <ShowEmpty v-show="pageLoading" /> + <ShowEmpty v-show="loading" /> <transition name="fade"> - <div v-show="!pageLoading"> + <div v-show="!loading"> <MediaHeader :imgurl="podcast.smallImageURL" :title="podcast.title" @@ -37,7 +37,7 @@ :episodes="podcast.episodes" @doPlay="doPlay" /> <EmptyContent - v-show="!episodesLoaded" + v-show="nextPage" icon="icon-loading" class="tableLoading" /> </div> @@ -55,7 +55,7 @@ import axios from '@nextcloud/axios' import { audioPlayer, doPlay } from '../services/player' -import ShowEmpty from './ShowEmpty' +import ShowEmpty from './placeholder/Show' export default { name: 'Show', @@ -69,9 +69,9 @@ export default { podcast: { episodes: [], }, - pageLoading: false, - episodesLoaded: false, + loading: true, nextPage: null, + podcastId: null, }), computed: { player() { @@ -94,10 +94,14 @@ export default { }, }, mounted() { - this.pageLoading = true - const podcastId = this.$route.params.id - this.queryPodcast(podcastId) - this.scroll() + this.podcastId = this.$route.params.id + this.queryPodcast(this.podcastId) + }, + created() { + window.addEventListener('scroll', this.handleScroll) + }, + destroyed() { + window.removeEventListener('scroll', this.handleScroll) }, methods: { @@ -120,16 +124,9 @@ export default { preFill() { console.log('prefilling') - const podcastId = this.$route.params.id // this.queryPodcast(podcastId) - console.log(podcastId) }, - /** - * Start playing a podcast episode - * @param {Object} episode Episode object - */ - async queryPodcast(podcastId) { const vm = this @@ -154,27 +151,27 @@ export default { }, processPodcast(podcast) { + if (podcast.meta.paging.page === podcast.meta.paging.last_page) { + this.nextPage = false + window.removeEventListener('scroll', this.handleScroll) + } else { + this.nextPage = podcast.meta.paging.next_page + } if (podcast.meta.paging.page === 0) { this.podcast = podcast.data } else { - if (podcast.meta.paging.page === podcast.meta.paging.last_page) { - this.episodesLoaded = true - } this.podcast.episodes = this.podcast.episodes.concat(podcast.data.episodes) } - this.pageLoading = false + this.loading = false }, /** - * On scroll event, load more stations if bottom reached + * On scroll event, load more episodes if bottom reached */ - scroll() { - window.onscroll = () => { - if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) { - if (!this.episodesLoaded) { - const podcastId = this.$route.params.id - this.queryPodcast(podcastId) - } + handleScroll() { + if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) { + if (!this.episodesLoaded) { + this.queryPodcast(this.podcastId) } } }, diff --git a/src/views/ShowEmpty.vue b/src/views/ShowEmpty.vue deleted file mode 100644 index 65ba57315da52991ab0d389824c4b896d66e8a7d..0000000000000000000000000000000000000000 --- a/src/views/ShowEmpty.vue +++ /dev/null @@ -1,183 +0,0 @@ -<!-- - - @copyright Copyright (c) 2020 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> - <div class="podcastHeaderEmpty"> - <div class="podcastImageEmpty" /> - <div class="podcastDescriptionEmpty"> - <div class="podcastDescriptionTitle" /> - <div - class="podcastDescriptionSubTitle" - style="max-width: 300px; width: 100%;" /> - <div - class="podcastDescriptionSubTitle" - style="max-width: 400px; width: 100%;" /> - <div - class="podcastDescriptionSubTitle" - style="max-width: 200px; width: 100%;" /> - </div> - </div> - <table class="episodeTable"> - <thead> - <tr> - <th class="iconColumn" /> - <th class="nameColumn"> - {{ t('podcast', 'Name') }} - </th> - <th class="actionColumn" /> - <th class="durationColumn"> - {{ t('podcast', 'Duration') }} - </th> - <th class="dateColumn"> - {{ t('podcast', 'Date') }} - </th> - </tr> - </thead> - <tbody> - <tr v-for="index in 3" :key="index"> - <td class="iconColumn"> - <div class="episodeImageEmpty" /> - </td> - <td class="nameColumn"> - <div - class="episodeDescription" - style="width: 500px;" /> - <div - class="episodeDescription" - style="width: 400px;" /> - </td> - <td class="actionColumn" /> - <td class="durationColumn" /> - <td class="dateColumn" /> - </tr> - </tbody> - </table> - </div> -</template> - -<script> - -export default { - name: 'ShowEmpty', -} -</script> - -<style lang="scss"> - -$base-color: #ddd; -$shine-color: #e8e8e8; -$animation-duration: 1.6s; -$avatar-offset: 52 + 16; - -@mixin background-gradient { - background-image: linear-gradient(90deg, $base-color 0px, $shine-color 40px, $base-color 80px); - background-size: 600px; -} - -.podcastHeaderEmpty { - width: 100%; - min-height: 300px; - display: flex; - justify-content: center; - padding: 40px 20px; - background-color: #f2f2f2; -} - -.podcastImageEmpty { - width: 200px; - height: 200px; - flex-shrink: 0; - background-color: #ccc; - margin-right: 25px; - border-radius: 5px; - - @include background-gradient; - animation: shine-avatar $animation-duration infinite linear; -} - -.podcastDescriptionEmpty { - max-width: 500px; - width: 100%; -} - -.podcastDescriptionTitle { - max-width: 500px; - width: 100%; - height: 35px; - border-radius: 5px; - margin-bottom: 30px; - background-color: #ddd; - - @include background-gradient; - animation: shine-lines $animation-duration infinite linear; -} - -.podcastDescriptionSubTitle { - height: 20px; - border-radius: 5px; - margin-bottom: 10px; - background-color: #ddd; - - @include background-gradient; - animation: shine-lines $animation-duration infinite linear; -} - -.episodeImageEmpty { - width: 74px; - height: 74px; - background-color: #ccc; - border-radius: 5px; - - @include background-gradient; - animation: shine-avatar $animation-duration infinite linear; -} - -.episodeDescription { - height: 20px; - border-radius: 5px; - margin-bottom: 10px; - background-color: #ddd; - - @include background-gradient; - animation: shine-lines $animation-duration infinite linear; -} - -@keyframes shine-lines { - 0% { - background-position: -100px; - } - 60%, 100% { - background-position: 500px; - } -} - -@keyframes shine-avatar { - 0% { - background-position: -100px + $avatar-offset; - } - 40%, 100% { - background-position: 140px + $avatar-offset; - } -} - -</style>