diff --git a/src/components/Main.vue b/src/components/Main.vue deleted file mode 100644 index 52f78c8adaa65ac136a3597b48e3f7187e0fce92..0000000000000000000000000000000000000000 --- a/src/components/Main.vue +++ /dev/null @@ -1,461 +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> - <Content app-name="podcast"> - <Navigation - :station-data="tableData" /> - <AppContent> - <Table - v-show="!pageLoading && tableData.length > 0" - v-resize="onResize" - :station-data="tableData" - :favorites="favorites" - @doPlay="doPlay" - @doFavor="doFavor" /> - <EmptyContent - v-if="pageLoading" - icon="icon-loading" /> - <EmptyContent - v-if="tableData.length === 0 && !pageLoading" - :icon="emptyContentIcon"> - {{ emptyContentMessage }} - <template #desc> - {{ emptyContentDesc }} - </template> - </EmptyContent> - </AppContent> - </Content> -</template> - -<script> -import Content from '@nextcloud/vue/dist/Components/Content' -import AppContent from '@nextcloud/vue/dist/Components/AppContent' -import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' -import Navigation from './Navigation' -import Table from './Table' -import { Howl, Howler } from 'howler' - -import { generateUrl } from '@nextcloud/router' -import { showError } from '@nextcloud/dialogs' -import axios from '@nextcloud/axios' - -let audioPlayer = null -const requesttoken = axios.defaults.headers.requesttoken - -export default { - name: 'Main', - components: { - Navigation, - Content, - AppContent, - Table, - EmptyContent, - }, - data: () => ({ - tableData: [], - pageLoading: false, - favorites: [], - queryParams: {}, - }), - computed: { - player() { - return this.$store.state.player - }, - emptyContentMessage() { - if (this.$route.name === 'FAVORITES') { - return t('podcast', 'No favorites yet') - } else if (this.$route.name === 'RECENT') { - return t('podcast', 'No recent stations yet') - } else if (this.$route.name === 'SEARCH') { - return t('podcast', 'No search results') - } - return 'No stations here' - }, - emptyContentIcon() { - if (this.$route.name === 'FAVORITES') { - return 'icon-star' - } else if (this.$route.name === 'RECENT') { - return 'icon-recent' - } else if (this.$route.name === 'SEARCH') { - return 'icon-search' - } - return 'icon-podcast' - }, - emptyContentDesc() { - if (this.$route.name === 'FAVORITES') { - return t('podcast', 'Stations you mark as favorite will show up here') - } else if (this.$route.name === 'RECENT') { - return t('podcast', 'Stations you recently played will show up here') - } else if (this.$route.name === 'SEARCH') { - return t('podcast', 'No stations were found matching your search term') - } - return t('podcast', 'No stations here') - }, - }, - watch: { - $route: 'onRoute', - 'player.volume'(newVolume, oldVolume) { - if (audioPlayer !== null) { - audioPlayer.volume(newVolume) - } - }, - 'player.isPaused'(newState, oldState) { - if (newState === true && audioPlayer !== null) { - audioPlayer.pause() - } else if (newState === false && audioPlayer !== null) { - audioPlayer.play() - } - }, - }, - created() { - this.loadSettings() - this.loadFavorites() - }, - mounted() { - this.onRoute() - this.scroll() - }, - methods: { - - onResize({ width, height }) { - const contentHeight = document.getElementById('app-content-vue').scrollHeight - const tableHeight = height - if (tableHeight < contentHeight) { - this.preFill() - } - }, - - preFill() { - const route = this.$route - this.loadStations(route.name) - }, - - async onRoute() { - this.tableData = [] - this.pageLoading = true - const route = this.$route - this.loadStations(route.name) - }, - - /** - * Favor a new station by sending the information to the server - * @param {Object} station Station object - */ - async doFavor(station) { - if (this.favorites.flat().includes(station.stationuuid)) { - let stationid = null - try { - for (let i = 0, len = this.favorites.length; i < len; i++) { - if (station.stationuuid === this.favorites[i][1]) { - stationid = this.favorites[i][0] - } - } - axios.defaults.headers.requesttoken = requesttoken - await axios - .delete(generateUrl(`/apps/podcast/api/favorites/${stationid}`)) - .then(response => { - this.favorites = this.favorites.filter(item => item[1] !== station.stationuuid) - }) - } catch (error) { - showError(t('podcast', 'Could not remove station from favorites')) - } - } else { - try { - let stationSrc = '' - if (!station.url_resolved) { - stationSrc = station.urlresolved - } else { - stationSrc = station.url_resolved - } - const stationMap = { - id: -1, - name: station.name.toString(), - urlresolved: stationSrc.toString(), - favicon: station.favicon.toString(), - stationuuid: station.stationuuid.toString(), - bitrate: station.bitrate.toString(), - country: station.country.toString(), - language: station.language.toString(), - homepage: station.homepage.toString(), - codec: station.codec.toString(), - tags: station.tags.toString(), - } - axios.defaults.headers.requesttoken = requesttoken - await axios - .post(generateUrl('/apps/podcast/api/favorites'), stationMap) - .then(response => { - this.favorites.push([response.data.id, station.stationuuid]) - }) - } catch (error) { - showError(t('podcast', 'Could not favor station')) - } - } - }, - - /** - * Start playing a podcast station and counting the playback - * @param {Object} station Station object - */ - async doPlay(station) { - const vm = this - - vm.$store.dispatch('isBuffering', true) - - if (audioPlayer !== null) { - audioPlayer.fade(vm.player.volume, 0, 500) - } - vm.$store.dispatch('setTitle', station.name) - - let stationSrc = '' - if (!station.url_resolved) { - stationSrc = station.urlresolved - } else { - stationSrc = station.url_resolved - } - Howler.unload() - audioPlayer = new Howl({ - src: stationSrc, - html5: true, - volume: vm.player.volume, - onplay() { - vm.$store.dispatch('isPlaying', true) - vm.$store.dispatch('isBuffering', false) - }, - onpause() { - vm.$store.dispatch('isPlaying', false) - vm.$store.dispatch('isBuffering', false) - }, - onend() { - showError(t('podcast', 'Lost connection to podcast episode stream, retrying ...')) - vm.$store.dispatch('isPlaying', false) - vm.$store.dispatch('isBuffering', true) - }, - }) - audioPlayer.unload() - audioPlayer.play() - audioPlayer.fade(0, vm.player.volume, 500) - - /* Count click */ - try { - delete axios.defaults.headers.requesttoken - axios.get(this.$apiUrl + '/json/url/' + station.stationuuid) - } catch (error) { - showError(t('podcast', 'Unable to count play on remote API')) - } - - /* Put into recent stations */ - try { - let stationSrc = '' - if (!station.url_resolved) { - stationSrc = station.urlresolved - } else { - stationSrc = station.url_resolved - } - const stationMap = { - id: -1, - name: station.name.toString(), - urlresolved: stationSrc.toString(), - favicon: station.favicon.toString(), - stationuuid: station.stationuuid.toString(), - bitrate: station.bitrate.toString(), - country: station.country.toString(), - language: station.language.toString(), - homepage: station.homepage.toString(), - codec: station.codec.toString(), - tags: station.tags.toString(), - } - axios.defaults.headers.requesttoken = requesttoken - await axios - .post(generateUrl('/apps/podcast/api/recent'), stationMap) - } catch (error) { - showError(t('podcast', 'Could not add station to recent list')) - } - - }, - - async loadStations(menuState = 'TOP') { - - const vm = this - const queryBase = this.$apiUrl + '/json/stations' - let queryURI = queryBase - let sortBy = 'clickcount' - - if (vm.$route.name === 'CATEGORIES') { - if (vm.$route.path === '/categories') { - vm.tableData = [ - { - name: t('podcast', 'Countries'), - type: 'folder', - path: '/categories/countries', - }, - { - name: t('podcast', 'States'), - type: 'folder', - path: '/categories/states', - }, - { - name: t('podcast', 'Languages'), - type: 'folder', - path: '/categories/languages', - }, - { - name: t('podcast', 'Tags'), - type: 'folder', - path: '/categories/tags', - }, - ] - vm.pageLoading = false - return true - } else if (vm.$route.params.category === 'tags') { - if (vm.$route.params.query) { - queryURI = this.$apiUrl + '/json/stations/search?tag=' + vm.$route.params.query + '&tagExact=true' - } else { - queryURI = this.$apiUrl + '/json/tags' - } - } else if (vm.$route.params.category === 'countries') { - if (vm.$route.params.query) { - queryURI = this.$apiUrl + '/json/stations/search?country=' + vm.$route.params.query + '&countryExact=true' - } else { - queryURI = this.$apiUrl + '/json/countries' - } - } else if (vm.$route.params.category === 'states') { - if (vm.$route.params.query) { - queryURI = this.$apiUrl + '/json/stations/search?state=' + vm.$route.params.query + '&stateExact=true' - } else { - queryURI = this.$apiUrl + '/json/states' - } - } else if (vm.$route.params.category === 'languages') { - if (vm.$route.params.query) { - queryURI = this.$apiUrl + '/json/stations/search?language=' + vm.$route.params.query + '&languageExact=true' - } else { - queryURI = this.$apiUrl + '/json/languages' - } - } - } - - // Skip loading more stations on certain sites - if (vm.tableData.length > 0 - && (vm.$route.name === 'FAVORITES' - || vm.$route.name === 'RECENT' - || vm.$route.name === 'CATEGORIES')) { - return true - } - - if (menuState === 'TOP') { - sortBy = 'clickcount' - } else if (menuState === 'NEW') { - sortBy = 'lastchangetime' - } else if (menuState === 'SEARCH') { - const searchQuery = vm.$route.params.query - queryURI = queryBase + '/byname/' + searchQuery - } else if (menuState === 'FAVORITES') { - queryURI = generateUrl('/apps/podcast/api/favorites') - } else if (menuState === 'RECENT') { - queryURI = generateUrl('/apps/podcast/api/recent') - } - - if (menuState !== 'CATEGORIES') { - vm.queryParams = { - limit: 20, - order: sortBy, - reverse: true, - offset: vm.tableData.length, - } - } else { - vm.queryParams = {} - } - - try { - if (menuState === 'FAVORITES' || menuState === 'RECENT') { - axios.defaults.headers.requesttoken = requesttoken - } else { - delete axios.defaults.headers.requesttoken - } - await axios.get(queryURI, { - params: vm.queryParams, - }) - .then(function(response) { - for (let i = 0; i < response.data.length; i++) { - const obj = response.data[i] - if (!obj.stationuuid) { - response.data[i].type = 'folder' - response.data[i].path = vm.$route.path + '/' + obj.name - } - } - vm.tableData = vm.tableData.concat(response.data) - vm.pageLoading = false - }) - } catch (error) { - showError(t('podcast', 'Could not fetch stations from remote API')) - } - }, - - /** - * On scroll event, load more stations if bottom reached - */ - scroll() { - window.onscroll = () => { - if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) { - const route = this.$route - this.loadStations(route.name) - } - } - }, - loadSettings() { - - // axios.defaults.headers.common = { - // 'User-Agent': 'Nextcloud Podcast App/' + this.$version, - // } - this.$store.dispatch('getVolumeState') - - }, - - async loadFavorites() { - const vm = this - try { - axios.defaults.headers.requesttoken = requesttoken - await axios.get(generateUrl('/apps/podcast/api/favorites')) - .then(function(response) { - const favorites = [] - for (let i = 0, len = response.data.length; i < len; i++) { - favorites.push([response.data[i].id, response.data[i].stationuuid]) - } - vm.favorites = favorites - }) - } catch (error) { - showError(t('podcast', 'Unable to load favorites')) - } - }, - }, -} -</script> - -<style> - -@media only screen and (min-width: 1024px) { - .app-navigation-toggle { - display: none; - } -} - -</style> diff --git a/src/router.js b/src/router.js index e600e9692f3d2a01ccf6341c7a2f6f30019757a2..6d40e876e6b4149601546a3957dc02e22ea4c97e 100644 --- a/src/router.js +++ b/src/router.js @@ -24,11 +24,11 @@ import Vue from 'vue' import Router from 'vue-router' import { generateUrl } from '@nextcloud/router' -import Main from './components/Main' +import Library from './views/Library' +import BrowseAll from './views/BrowseAll' 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' Vue.use(Router) @@ -48,7 +48,7 @@ const router = new Router({ }, { path: '/library', - component: NotImplemented, + component: Library, name: 'LIBRARY', }, { @@ -73,12 +73,6 @@ const router = new Router({ name: 'EPISODE', props: {}, }, - { - path: '/search/:query', - component: Main, - name: 'SEARCH', - props: {}, - }, ], }) diff --git a/src/services/ShowApi.js b/src/services/ShowApi.js index 500918dc8e17d0c64c9b1b573930e0f6a0bd3ed4..e1b7cf601de58bfde33d1e6d1aadad924c5283aa 100644 --- a/src/services/ShowApi.js +++ b/src/services/ShowApi.js @@ -70,4 +70,20 @@ export class ShowApi { }) } + queryLibrary(show) { + axios.defaults.headers.requesttoken = requesttoken + return axios.get(this.url('/shows')) + .then( + (response) => { + return Promise.resolve(response.data) + }, + (err) => { + return Promise.reject(err) + } + ) + .catch((err) => { + return Promise.reject(err) + }) + } + } diff --git a/src/views/Library.vue b/src/views/Library.vue new file mode 100644 index 0000000000000000000000000000000000000000..e79ab90b519830e513976c0b058c72b113f0bf88 --- /dev/null +++ b/src/views/Library.vue @@ -0,0 +1,154 @@ +<!-- + - @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 class="mainContent"> + <div class="podcastSection"> + <div class="podcastSectionHeader"> + <h1>Library</h1> + </div> + <div class="podcastSliderAll"> + <div + v-for="(podcast, idx) in podcasts" + :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> +</template> + +<script> +import { ShowApi } from './../services/ShowApi' +const showClient = new ShowApi() + +export default { + name: 'Library', + data: () => ({ + podcasts: {}, + }), + mounted() { + this.queryPodcasts() + }, + methods: { + + async queryPodcasts() { + + const library = await showClient.queryLibrary() + this.podcasts = library + + }, + + }, +} +</script> + +<style lang="scss"> +.mainContent { + padding: 30px; +} + +.podcastSection { + margin-bottom: 30px; +} + +.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; + margin-bottom: 0px; + } + a { + color: #1976d2; + cursor: pointer; + } +} + +.podcastSliderAll { + width: 100%; + display: flex; + flex-wrap: wrap; +} + +.podcastCard { + width: 170px; + height: 220px; + margin-right: 15px; + flex-shrink: 0; + background: rgba(241, 241, 241, 0.6); + border-radius: 3px; + padding: 15px; + transition: all 0.2s ease-in-out; + + * { + cursor: pointer; + } + + .podcastImage { + background-size: cover; + background-position: center; + box-shadow: 1px 1px 2px rgba(0,0,0,.5); + border: 1px solid rgba(0,0,0,.5); + border-radius: 5px; + width: 140px; + height: 140px; + margin-bottom: 5px; + } + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + } + span.title { + font-size: 1em; + } + span.subtitle { + font-size: 0.9em; + color: #b5b1b1; + } +} + +.podcastCard:hover { + background: rgb(236, 236, 236); +} +</style> diff --git a/src/views/Show.vue b/src/views/Show.vue index 5d931bc4a6d8fa97732b798f6f04c8c388f32587..78f25835fb23c57628a78d9f6c30bc54e26aa338 100644 --- a/src/views/Show.vue +++ b/src/views/Show.vue @@ -73,11 +73,9 @@ export default { page: 0, podcastId: null, issubscribed: false, + loading: true, }), computed: { - loading() { - return this.episodes.length === 0 // FIXME: also consider podcast object - }, player() { return this.$store.state.player }, @@ -101,14 +99,14 @@ export default { }, }, mounted() { - this.init() + this.initPage() }, destroyed() { window.removeEventListener('scroll', this.handleScroll) }, methods: { - init() { + initPage() { this.podcast = {} this.episodes = [] this.page = 0 @@ -169,6 +167,7 @@ export default { processPodcast(podcast) { this.podcast = podcast document.title = podcast.title + ' - Podcast - Nextcloud' + this.loading = false }, /**