From 5d100c9092d60db2f00218d0304a132a5eaf5d59 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich <onny@project-insanity.org> Date: Sat, 20 Mar 2021 17:51:05 +0100 Subject: [PATCH] add new episodes to library view, code cleanup --- src/App.vue | 1 + src/components/Header.vue | 58 +++++++++++++++++ src/components/ItemGrid.vue | 48 -------------- src/components/ItemSlider.vue | 35 +--------- src/components/LoadMore.vue | 90 ++++++++++++++++++++++++++ src/store/episode.js | 13 ++-- src/store/show.js | 11 ++-- src/views/Browse.vue | 20 +++--- src/views/BrowseAll.vue | 10 +-- src/views/Library.vue | 118 ++++++++++++++++++---------------- src/views/Listening.vue | 15 +---- 11 files changed, 240 insertions(+), 179 deletions(-) create mode 100644 src/components/Header.vue create mode 100644 src/components/LoadMore.vue diff --git a/src/App.vue b/src/App.vue index 1e4edc7..52074ce 100644 --- a/src/App.vue +++ b/src/App.vue @@ -49,6 +49,7 @@ export default { }, created() { this.$store.dispatch('loadVolume') + this.$store.dispatch('loadEpisodes') }, } </script> diff --git a/src/components/Header.vue b/src/components/Header.vue new file mode 100644 index 0000000..f8241e5 --- /dev/null +++ b/src/components/Header.vue @@ -0,0 +1,58 @@ +<!-- + - @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 class="podcastSectionHeader"> + <h1>{{ title }}</h1> + <slot /> + </div> +</template> + +<script> +export default { + name: 'Header', + props: { + title: { + type: String, + default: '', + }, + }, +} +</script> + +<style lang="scss"> + +.podcastSectionHeader { + padding: 20px 30px; + display: flex; + align-items: center; + + h1 { + flex-grow: 1; + font-size: 1.6em; + } + a { + color: #1976d2; + cursor: pointer; + } +} + +</style> diff --git a/src/components/ItemGrid.vue b/src/components/ItemGrid.vue index 170eed4..01aaa65 100644 --- a/src/components/ItemGrid.vue +++ b/src/components/ItemGrid.vue @@ -21,23 +21,6 @@ --> <template> <div class="podcastSection"> - <div class="podcastSectionHeader"> - <h1>{{ title }}</h1> - <Actions v-show="showMenu"> - <ActionButton - icon="icon-add" - :close-after-click="true" - @click="showModal()"> - Add podcast feed - </ActionButton> - <ActionButton - icon="icon-download" - :close-after-click="true" - @click="$emit('doExport')"> - Export subscriptions - </ActionButton> - </Actions> - </div> <div class="grid"> <div v-for="(podcast, idx) in podcasts" @@ -64,28 +47,13 @@ <script> -import Actions from '@nextcloud/vue/dist/Components/Actions' -import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' - export default { name: 'ItemGrid', - components: { - Actions, - ActionButton, - }, props: { - title: { - type: String, - default: '', - }, podcasts: { type: Array, default() { return [] }, }, - showMenu: { - type: Boolean, - default: false, - }, }, } </script> @@ -96,22 +64,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)); diff --git a/src/components/ItemSlider.vue b/src/components/ItemSlider.vue index d2a8991..a855b2b 100644 --- a/src/components/ItemSlider.vue +++ b/src/components/ItemSlider.vue @@ -21,10 +21,6 @@ --> <template> <div class="podcastSection"> - <div class="podcastSectionHeader"> - <h1>{{ title }}</h1> - <a :href="showallurl">{{ t('podcast', 'Show all') }}</a> - </div> <div class="podcastSliderWrapper"> <div v-show="showPrev" @@ -39,7 +35,7 @@ :key="idx" class="podcastCard"> <router-link :to="{ path: `/browse/show/${podcast.id}`}"> - <div v-lazy:background-image="podcast.smallImageURL" + <div v-lazy:background-image="podcast.imgurl" class="podcastImage" /> <span class="title"> {{ podcast.title }} @@ -63,14 +59,6 @@ export default { name: 'ItemSlider', props: { - title: { - type: String, - default: '', - }, - showallurl: { - type: String, - default: '', - }, podcasts: { type: Array, default() { return [] }, @@ -116,26 +104,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 { diff --git a/src/components/LoadMore.vue b/src/components/LoadMore.vue new file mode 100644 index 0000000..ebcf958 --- /dev/null +++ b/src/components/LoadMore.vue @@ -0,0 +1,90 @@ +<!-- + - @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="!finished" + 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: { + finished: { + type: Boolean, + default: false, + }, + }, + mounted() { + document.getElementById('app-content-vue').addEventListener('scroll', this.handleScroll) + }, + destroyed() { + document.getElementById('app-content-vue').removeEventListener('scroll', this.handleScroll) + }, + methods: { + + preFill() { + if (this.finished) { + 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('loadMore') + } + } + }, + + /** + * On scroll event, load more shows if bottom reached + */ + handleScroll() { + if (this.finished) { + 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('loadMore') + } + } + }, + + onResize() { + this.preFill() + }, + + }, +} +</script> diff --git a/src/store/episode.js b/src/store/episode.js index d5af04d..51b77f7 100644 --- a/src/store/episode.js +++ b/src/store/episode.js @@ -22,7 +22,7 @@ import { EpisodeApi } from './../services/EpisodeApi' -const apiClient = new EpisodeApi() +const episodeApiClient = new EpisodeApi() export default { state: { @@ -61,29 +61,28 @@ export default { }, actions: { async loadEpisodes(context) { - const episodes = await apiClient.loadEpisodes() + const episodes = await episodeApiClient.queryEpisodes(null, 0) if (episodes) { - context.dispatch('loadEpisode', episodes.data[0]) + context.dispatch('loadEpisode', episodes.data.episodes[0]) } - context.commit('setEpisodes', episodes.data) }, addEpisode({ commit, getters }, episode) { if (getters.episodeExists(episode.id)) { return true } - apiClient.addEpisode(episode) + episodeApiClient.addEpisode(episode) .then((episode) => { commit('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 }) }) diff --git a/src/store/show.js b/src/store/show.js index 4953c75..2c022f6 100644 --- a/src/store/show.js +++ b/src/store/show.js @@ -22,16 +22,13 @@ 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) }, @@ -58,17 +55,17 @@ export default { }, actions: { async loadShows({ commit }) { - const shows = await apiClient.loadShows() + 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) }) diff --git a/src/views/Browse.vue b/src/views/Browse.vue index 6dc8afa..456ece9 100644 --- a/src/views/Browse.vue +++ b/src/views/Browse.vue @@ -20,18 +20,20 @@ - --> <template> - <div> + <div style="padding-top: 10px;"> <BrowseEmpty v-show="loading" /> <div v-show="!loading" class="mainContent"> + <Header :title="t('podcast', 'Hot podcasts')"> + <a href="#/browse/hot">Show all</a> + </Header> <ItemSlider - :title="t('podcast', 'Hot podcasts')" - showallurl="#/browse/hot" :podcasts="podcastsHot" /> + <Header :title="t('podcast', 'New podcasts')"> + <a href="#/browse/new">Show all</a> + </Header> <ItemSlider - :title="t('podcast', 'New podcasts')" - showallurl="#/browse/new" :podcasts="podcastsLatest" /> </div> </div> @@ -40,6 +42,7 @@ <script> import BrowseEmpty from './placeholder/Browse' import ItemSlider from '../components/ItemSlider' +import Header from '../components/Header' import { setBrowserTitle } from '../utils/misc.js' import { ShowApi } from './../services/ShowApi' const showApiClient = new ShowApi() @@ -49,6 +52,7 @@ export default { components: { BrowseEmpty, ItemSlider, + Header, }, data: () => ({ podcastsHot: [], @@ -74,9 +78,3 @@ export default { }, } </script> - -<style lang="scss"> -.mainContent { - padding: 20px 30px; -} -</style> diff --git a/src/views/BrowseAll.vue b/src/views/BrowseAll.vue index 20bd4a5..2575af0 100644 --- a/src/views/BrowseAll.vue +++ b/src/views/BrowseAll.vue @@ -21,9 +21,9 @@ --> <template> <div class="mainContent"> + <Header :title="title" /> <ItemGrid v-resize:debounce="onResize" - :title="title" :podcasts="podcasts" /> <EmptyContent v-show="page" @@ -34,6 +34,7 @@ <script> import ItemGrid from '../components/ItemGrid' +import Header from '../components/Header' import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' import { setBrowserTitle } from '../utils/misc.js' import { ShowApi } from './../services/ShowApi' @@ -45,6 +46,7 @@ export default { name: 'BrowseAll', components: { ItemGrid, + Header, EmptyContent, }, directives: { @@ -120,9 +122,3 @@ export default { }, } </script> - -<style lang="scss"> -.mainContent { - padding: 30px; -} -</style> diff --git a/src/views/Library.vue b/src/views/Library.vue index b63e083..293acf7 100644 --- a/src/views/Library.vue +++ b/src/views/Library.vue @@ -21,80 +21,83 @@ --> <template> <div class="mainContent"> - <ItemGrid - v-resize:debounce="onResize" - :title="t('podcast', 'Library')" - :show-menu="true" - :podcasts="shows" - @doExport="doExport" /> - <EmptyContent - v-show="page" - icon="icon-loading" - class="tableLoading" /> + <Header :title="t('podcast', 'Library')"> + <Actions> + <ActionButton + icon="icon-details" + :close-after-click="true"> + {{ t('podcast', 'Show all') }} + </ActionButton> + <ActionButton + icon="icon-download" + :close-after-click="true" + @click="$emit('doExport')"> + {{ t('podcast', 'Export subscriptions') }} + </ActionButton> + </Actions> + </Header> + <ItemSlider :podcasts="shows" /> + <Header :title="t('podcast', 'New episodes')" /> + <LoadMore :finished="finished" + @loadMore="queryEpisodes(page)"> + <Table + :episodes="episodes" + :extended="true" + @doPlay="doPlay" /> + </LoadMore> </div> </template> <script> -import ItemGrid from '../components/ItemGrid' -import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' -import { mapGetters } from 'vuex' +import Actions from '@nextcloud/vue/dist/Components/Actions' +import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' +import ItemSlider from '../components/ItemSlider' +import LoadMore from '../components/LoadMore' +import Table from '../components/Table' +import Header from '../components/Header' +import { mapGetters, mapActions } from 'vuex' import { setBrowserTitle } from '../utils/misc.js' import { getRequestToken } from '@nextcloud/auth' -import resize from 'vue-resize-directive' import { ShowApi } from './../services/ShowApi' +import { EpisodeApi } from './../services/EpisodeApi' const showApiClient = new ShowApi() +const episodeApiClient = new EpisodeApi() export default { name: 'Library', components: { - ItemGrid, - EmptyContent, - }, - directives: { - resize, + ItemSlider, + Table, + Actions, + ActionButton, + Header, + LoadMore, }, data: () => ({ page: 0, shows: [], + episodes: [], + finished: false, }), computed: { ...mapGetters([ - 'subscribedShows', + 'episodePlaying', ]), }, mounted() { setBrowserTitle(t('podcast', 'Library')) - document.getElementById('app-content-vue').addEventListener('scroll', this.handleScroll) this.shows = [] + this.episodes = [] this.page = 0 + this.finished = false this.queryPodcasts(this.page) - }, - destroyed() { - document.getElementById('app-content-vue').removeEventListener('scroll', this.handleScroll) + this.queryEpisodes(this.page) }, methods: { - - preFill() { - const yFill = document.getElementsByClassName('tableLoading')[0].getBoundingClientRect().bottom - const playerPosY = document.getElementsByClassName('player')[0].getBoundingClientRect().top - if (yFill > 0 && yFill < playerPosY) { - this.queryPodcasts(this.page) - } - }, - - /** - * On scroll event, load more shows if bottom reached - */ - handleScroll() { - const appContent = document.getElementById('app-content-vue') - if (appContent.scrollTop === (appContent.scrollHeight - appContent.clientHeight)) { - this.queryPodcasts(this.page) - } - }, - - onResize() { - this.preFill() - }, + ...mapActions([ + 'pauseEpisode', + 'playEpisode', + ]), doExport() { window.location @@ -105,20 +108,27 @@ export default { async queryPodcasts(page) { const shows = await showApiClient.queryShows(page) this.shows = this.shows.concat(shows.data) - if (shows.meta.paging.next_page === null) { + }, + + async queryEpisodes(page) { + const episodes = await episodeApiClient.queryEpisodes(null, page) + this.episodes = this.episodes.concat(episodes.data.episodes) + if (episodes.meta.paging.next_page === null) { this.page = null - document.getElementById('app-content-vue').removeEventListener('scroll', this.handleScroll) + this.finished = true } else { this.page += 1 } }, + doPlay(episode) { + if (this.episodePlaying(episode.id)) { + this.pauseEpisode() + } else { + this.playEpisode(episode) + } + }, + }, } </script> - -<style lang="scss"> -.mainContent { - padding: 30px; -} -</style> diff --git a/src/views/Listening.vue b/src/views/Listening.vue index b5f89ad..bd38d99 100644 --- a/src/views/Listening.vue +++ b/src/views/Listening.vue @@ -21,9 +21,7 @@ --> <template> <div> - <div class="podcastSectionHeader listening"> - <h1>{{ t('podcast', 'Currently listening') }}</h1> - </div> + <Header :title="t('podcast', 'Currently listening')" /> <Table v-resize:debounce="onResize" :episodes="episodes" @@ -38,6 +36,7 @@ <script> import Table from '../components/Table' +import Header from '../components/Header' import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' import { mapGetters, mapActions } from 'vuex' import { setBrowserTitle } from '../utils/misc.js' @@ -50,6 +49,7 @@ export default { components: { Table, EmptyContent, + Header, }, directives: { resize, @@ -123,12 +123,3 @@ export default { }, } </script> - -<style lang="scss"> - -.listening { - padding-top: 30px; - padding-left: 30px; -} - -</style> -- GitLab