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