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
  • onny/nextcloud-app-podcast
  • petre/nextcloud-app-podcast
2 results
Show changes
/**
* 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 @@ ...@@ -20,18 +20,19 @@
- -
--> -->
<template> <template>
<div> <div style="padding-top: 10px;">
<BrowseEmpty v-show="loading" /> <BrowseEmpty v-show="loading" />
<div <div
v-show="!loading" v-show="!loading">
class="mainContent"> <Header :title="t('podcast', 'Hot podcasts')">
<a href="#/browse/hot">Show all</a>
</Header>
<ItemSlider <ItemSlider
title="Hot podcasts"
showallurl="#/browse/hot"
:podcasts="podcastsHot" /> :podcasts="podcastsHot" />
<Header :title="t('podcast', 'New podcasts')">
<a href="#/browse/new">Show all</a>
</Header>
<ItemSlider <ItemSlider
title="New podcasts"
showallurl="#/browse/new"
:podcasts="podcastsLatest" /> :podcasts="podcastsLatest" />
</div> </div>
</div> </div>
...@@ -40,15 +41,17 @@ ...@@ -40,15 +41,17 @@
<script> <script>
import BrowseEmpty from './placeholder/Browse' import BrowseEmpty from './placeholder/Browse'
import ItemSlider from '../components/ItemSlider' import ItemSlider from '../components/ItemSlider'
import Header from '../components/Header'
import { FyydApi } from './../services/FyydApi' import { setBrowserTitle } from '../utils/misc.js'
const fyydClient = new FyydApi() import { ShowApi } from './../services/ShowApi'
const showApiClient = new ShowApi()
export default { export default {
name: 'Browse', name: 'Browse',
components: { components: {
BrowseEmpty, BrowseEmpty,
ItemSlider, ItemSlider,
Header,
}, },
data: () => ({ data: () => ({
podcastsHot: [], podcastsHot: [],
...@@ -57,14 +60,15 @@ export default { ...@@ -57,14 +60,15 @@ export default {
}), }),
mounted() { mounted() {
this.queryPodcastLists() this.queryPodcastLists()
setBrowserTitle(t('podcast', 'Browse'))
}, },
methods: { methods: {
async queryPodcastLists() { async queryPodcastLists() {
const hotList = await fyydClient.queryList('hot', 10) const hotList = await showApiClient.queryCategory('hot', 10)
this.podcastsHot = hotList.data this.podcastsHot = hotList.data
const latestList = await fyydClient.queryList('latest', 10) const latestList = await showApiClient.queryCategory('latest', 10)
this.podcastsLatest = latestList.data this.podcastsLatest = latestList.data
this.loading = false this.loading = false
...@@ -73,9 +77,3 @@ export default { ...@@ -73,9 +77,3 @@ export default {
}, },
} }
</script> </script>
<style lang="scss">
.mainContent {
padding: 20px 30px;
}
</style>
...@@ -20,64 +20,73 @@ ...@@ -20,64 +20,73 @@
- -
--> -->
<template> <template>
<div class="mainContent"> <div>
<ItemGrid <Header :title="title" />
:title="title" <LoadMore :page="page"
:podcasts="podcasts" /> @load-more="queryPodcasts(page)">
<ItemGrid
:podcasts="podcasts" />
</LoadMore>
</div> </div>
</template> </template>
<script> <script>
import ItemGrid from '../components/ItemGrid' import ItemGrid from '../components/ItemGrid'
import Header from '../components/Header'
import LoadMore from '../components/LoadMore'
import { setBrowserTitle } from '../utils/misc.js'
import { ShowApi } from './../services/ShowApi'
import { FyydApi } from './../services/FyydApi' const showApiClient = new ShowApi()
const fyydClient = new FyydApi()
export default { export default {
name: 'BrowseAll', name: 'BrowseAll',
components: { components: {
ItemGrid, ItemGrid,
Header,
LoadMore,
}, },
data: () => ({ data: () => ({
podcasts: {}, podcasts: [],
category: null, category: null,
categoryId: null, categoryId: null,
title: '', title: '',
page: 0,
}), }),
mounted() { mounted() {
this.category = this.$route.params.category this.category = this.$route.params.category
this.categoryId = this.$route.params.categoryId this.categoryId = this.$route.params.categoryId
this.queryPodcasts()
}, },
methods: { methods: {
async queryPodcasts() { async queryPodcasts(page = 0) {
let podcasts = null let podcasts = null
if (this.category === 'hot') { if (this.category === 'hot') {
podcasts = await fyydClient.queryList('hot', 20) podcasts = await showApiClient.queryCategory('hot', 20, page)
this.title = 'Hot podcasts' this.title = t('podcast', 'Hot podcasts')
document.title = 'Hot podcasts - Podcast - Nextcloud' this.podcasts = this.podcasts.concat(podcasts.data)
this.podcasts = podcasts.data
} else if (this.category === 'new') { } else if (this.category === 'new') {
podcasts = await fyydClient.queryList('latest', 20) podcasts = await showApiClient.queryCategory('latest', 20, page)
this.title = 'New podcasts' this.title = t('podcast', 'New podcasts')
document.title = 'New podcasts - Podcast - Nextcloud' this.podcasts = this.podcasts.concat(podcasts.data)
this.podcasts = 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 { } else {
podcasts = await fyydClient.queryList(this.categoryId, 20) podcasts = await showApiClient.queryCategory(this.categoryId, 20, page)
this.title = 'Podcasts in ' + podcasts.data.category.title this.title = t('podcast', 'Podcasts in') + ' ' + podcasts.data.category.title
this.podcasts = podcasts.data.podcasts this.podcasts = this.podcasts.concat(podcasts.data.podcasts)
} }
if (podcasts.meta.paging.next_page === null) {
this.page = null
} else {
this.page += 1
}
setBrowserTitle(this.title)
}, },
}, },
} }
</script> </script>
<style lang="scss">
.mainContent {
padding: 30px;
}
</style>
...@@ -32,17 +32,17 @@ ...@@ -32,17 +32,17 @@
:isshow="false"> :isshow="false">
<button <button
class="podcastButton button primary new-button" class="podcastButton button primary new-button"
:class="isPlaying(episode.id) ? 'icon-pause-white' : 'icon-play-white'" :class="episodePlaying(episode.id) ? 'icon-pause-white' : 'icon-play-white'"
@click="doPlay"> @click="doPlay">
{{ playButtonText }} {{ playButtonText }}
</button> </button>
<div class="episodeDetails"> <div class="episodeDetails">
<span> <span>
<b>Duration:</b> <b>{{ t('podcast', 'Duration') }}:</b>
{{ episode.duration_string }} {{ episode.duration_string }}
</span> </span>
<span> <span>
<b>Publication date:</b> <b>{{ t('podcast', 'Publication date') }}:</b>
<span class="inline" :title="readableDate(episode.pubdate)"> <span class="inline" :title="readableDate(episode.pubdate)">
{{ readableTimeAgo(episode.pubdate) }} {{ readableTimeAgo(episode.pubdate) }}
</span> </span>
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
</div> </div>
</MediaHeader> </MediaHeader>
<ContentCollapsable <ContentCollapsable
title="Episode notes"> :title="t('podcast', 'Episode description')">
<!-- eslint-disable --> <!-- eslint-disable -->
<p <p
v-html="episodeDescriptionParsed" v-html="episodeDescriptionParsed"
...@@ -59,17 +59,21 @@ ...@@ -59,17 +59,21 @@
</ContentCollapsable> </ContentCollapsable>
<ContentCollapsable <ContentCollapsable
v-show="episode.chapters" v-show="episode.chapters"
title="Episode chapters"> :title="t('podcast', 'Episode chapters')">
<table class="chapterTable"> <table class="chapterTable">
<tbody> <tbody>
<tr <tr
v-for="(chapter, idx) in episode.chapters" v-for="(chapter, idx) in episode.chapters"
:key="idx" :key="idx"
@click="seekEpisode(chapter.start)"> @click="startSeekEpisode(chapter.start)">
<td class="timeColumn"> <td class="timeColumn">
{{ readableDuration(chapter.start) }} {{ readableDuration(chapter.start) }}
</td> </td>
<td class="titleColumn"> <td class="titleColumn">
<PlayAnimation
v-show="episodeLoaded(episode.id) && chapterPlaying === idx"
:paused="isPaused(episode.id)"
style="margin-right: 8px;" />
{{ chapter.title }} {{ chapter.title }}
</td> </td>
</tr> </tr>
...@@ -88,10 +92,13 @@ import ContentCollapsable from '../components/ContentCollapsable' ...@@ -88,10 +92,13 @@ import ContentCollapsable from '../components/ContentCollapsable'
import MediaHeader from '../components/MediaHeader' import MediaHeader from '../components/MediaHeader'
import { mapGetters, mapActions } from 'vuex' import { mapGetters, mapActions } from 'vuex'
import TimeAgo from 'javascript-time-ago' import TimeAgo from 'javascript-time-ago'
import PlayAnimation from '../components/PlayAnimation'
import { setBrowserTitle } from '../utils/misc.js'
import { FyydApi } from './../services/FyydApi' import { EpisodeApi } from './../services/EpisodeApi'
import { ShowApi } from './../services/ShowApi'
const fyydClient = new FyydApi() const episodeApiClient = new EpisodeApi()
const showApiClient = new ShowApi()
const timeAgo = new TimeAgo('en-US') const timeAgo = new TimeAgo('en-US')
...@@ -101,6 +108,7 @@ export default { ...@@ -101,6 +108,7 @@ export default {
EpisodeEmpty, EpisodeEmpty,
ContentCollapsable, ContentCollapsable,
MediaHeader, MediaHeader,
PlayAnimation,
}, },
data: () => ({ data: () => ({
episode: {}, episode: {},
...@@ -111,11 +119,13 @@ export default { ...@@ -111,11 +119,13 @@ export default {
}), }),
computed: { computed: {
...mapGetters([ ...mapGetters([
'isPlaying', 'episodePlaying',
'episodeLoaded',
'isPaused', 'isPaused',
'getSeek',
]), ]),
playButtonText() { playButtonText() {
if (this.isPlaying(this.episode.id)) { if (this.episodePlaying(this.episode.id)) {
return t('podcast', 'Pause episode') return t('podcast', 'Pause episode')
} else if (this.isPaused(this.episode.id)) { } else if (this.isPaused(this.episode.id)) {
return t('podcast', 'Resume episode') return t('podcast', 'Resume episode')
...@@ -128,26 +138,35 @@ export default { ...@@ -128,26 +138,35 @@ export default {
return '' return ''
} }
const linkRegex = /((?:href|src)=")?(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/ig const linkRegex = /((?:href|src)=")?(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/ig
const timestampRegex = /(0)?(([0-9]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])/g const timestampRegex = /(([0-9]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])/g
const episodeDescription = this.episode.description const episodeDescription = this.episode.description
let content = episodeDescription.replace(linkRegex, function(match, attr) { let content = episodeDescription.replace(linkRegex, function(match, attr) {
if (typeof attr !== 'undefined') {
return match
}
return '<a target="_blank" href="' + match + '">' + match + '</a>' return '<a target="_blank" href="' + match + '">' + match + '</a>'
}) })
content = content.replace(timestampRegex, function(match, attr) { content = content.replace(timestampRegex, function(match, attr) {
if (typeof attr !== 'undefined') {
return match
}
return '<a href="#' + match + '">' + match + '</a>' return '<a href="#' + match + '">' + match + '</a>'
}) })
return content return content
}, },
chapterPlaying() {
const vm = this
const arr = this.episode.chapters
for (const [index, chapter] of arr.entries()) {
if (parseInt(vm.getSeek) >= parseInt(chapter.start_ms / 1000)) {
if (vm.getSeek && arr.length === index + 1) {
return index
}
if (parseInt(vm.getSeek) < parseInt(arr[index + 1].start_ms / 1000)) {
return index
}
}
}
return false
},
}, },
mounted() { mounted() {
this.episodeId = this.$route.params.episodeId this.episodeId = this.$route.params.episodeId
...@@ -156,10 +175,15 @@ export default { ...@@ -156,10 +175,15 @@ export default {
this.queryPodcastName(this.podcastId) this.queryPodcastName(this.podcastId)
const vm = this const vm = this
this.$refs.episodeContent.addEventListener('click', function(event) { this.$refs.episodeContent.addEventListener('click', function(event) {
event.preventDefault() if (event.target.target !== '_blank') {
if (event.target.href) { event.preventDefault()
const timecode = event.target.href.split('#')[1] if (event.target.href) {
vm.seekEpisode(timecode) const timecode = event.target.href.split('#')[1]
vm.startSeekEpisode(timecode)
if (!vm.episodePlaying(vm.episode.id)) {
vm.playEpisode(vm.episode)
}
}
} }
}) })
}, },
...@@ -184,17 +208,21 @@ export default { ...@@ -184,17 +208,21 @@ export default {
return s return s
}, },
seekEpisode(startTime) { startSeekEpisode(startTime) {
if (!this.episodePlaying(this.episode.id)) {
this.playEpisode(this.episode)
}
const startTimeSec = this.hmsToSecondsOnly(startTime) const startTimeSec = this.hmsToSecondsOnly(startTime)
this.seekEpisode(startTimeSec) this.seekEpisode(startTimeSec)
}, },
doPlay() { doPlay() {
if (this.isPlaying(this.episode.id)) { if (this.episodePlaying(this.episode.id)) {
this.pauseEpisode() this.pauseEpisode()
} else { } else {
this.playEpisode(this.episode) this.playEpisode(this.episode)
} }
this.seekEpisode(this.episode.playtime)
}, },
readableDuration(timestamp) { readableDuration(timestamp) {
...@@ -214,18 +242,18 @@ export default { ...@@ -214,18 +242,18 @@ export default {
}, },
async queryEpisode(episodeId) { async queryEpisode(episodeId) {
const episode = await fyydClient.queryEpisode(episodeId) const episode = await episodeApiClient.queryEpisode(episodeId)
this.processEpisode(episode) this.processEpisode(episode)
}, },
processEpisode(episode) { processEpisode(episode) {
this.episode = episode.data this.episode = episode.data
document.title = episode.data.title + ' - Podcast - Nextcloud' setBrowserTitle(episode.data.title)
this.loading = false this.loading = false
}, },
async queryPodcastName(podcastId) { async queryPodcastName(podcastId) {
const podcast = await fyydClient.queryPodcast(podcastId) const podcast = await showApiClient.queryShow(podcastId)
this.podcastName = podcast.data.title this.podcastName = podcast.data.title
}, },
...@@ -263,35 +291,35 @@ table.chapterTable { ...@@ -263,35 +291,35 @@ table.chapterTable {
tbody { tbody {
td {
padding: 15px 0px;
padding-left: 30px;
font-style: normal;
border-bottom: 1px solid var(--color-border);
cursor: pointer;
}
tr { tr {
height: 30px; height: 30px;
background-color: var(--color-background-light); background-color: var(--color-background-light);
transition: opacity 500ms ease 0s; transition: opacity 500ms ease 0s;
}
tr td * { td {
cursor: pointer; padding: 15px 0px;
} padding-left: 30px;
font-style: normal;
border-bottom: 1px solid var(--color-border);
cursor: pointer;
td.timeColumn { * {
width: 120px; cursor: pointer;
color: #1976d2; }
}
td.titleColumn { &.timeColumn {
padding-left: 0px; width: 120px;
} color: #1976d2;
}
tr:hover, tr:focus, tr.mouseOver td { &.titleColumn {
background-color: var(--color-background-hover); padding-left: 0px;
}
}
&:hover, &:focus, &.mouseOver td {
background-color: var(--color-background-hover);
}
} }
} }
......
...@@ -20,38 +20,110 @@ ...@@ -20,38 +20,110 @@
- -
--> -->
<template> <template>
<div class="mainContent"> <div>
<ItemGrid <Header :title="t('podcast', 'Library')">
title="Library" <Actions>
:podcasts="podcasts" /> <ActionButton
icon="icon-details"
:close-after-click="true"
@click="$router.push('/browse/subscriptions')">
{{ t('podcast', 'Show all subscriptions') }}
</ActionButton>
<ActionButton
icon="icon-download"
:close-after-click="true"
@click="$emit('do-export')">
{{ t('podcast', 'Export subscriptions') }}
</ActionButton>
</Actions>
</Header>
<ItemSlider :podcasts="shows" />
<LoadMore :page="page"
@load-more="loadEpisodes(page)">
<Table
:episodes="getEpisodes"
@doPlay="doPlay" />
</LoadMore>
</div> </div>
</template> </template>
<script> <script>
import ItemGrid from '../components/ItemGrid' import Actions from '@nextcloud/vue/dist/Components/Actions'
import { mapGetters } from 'vuex' 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 { ShowApi } from './../services/ShowApi'
const showApiClient = new ShowApi()
export default { export default {
name: 'Library', name: 'Library',
components: { components: {
ItemGrid, ItemSlider,
Table,
Actions,
ActionButton,
Header,
LoadMore,
}, },
data: () => ({
page: 0,
shows: [],
}),
computed: { computed: {
...mapGetters([ ...mapGetters([
'subscribedShows', 'episodePlaying',
'getEpisodes',
]), ]),
podcasts() { },
const list = [ mounted() {
...this.subscribedShows, setBrowserTitle(t('podcast', 'Library'))
] this.shows = []
return list this.page = 0
this.clearEpisodes()
this.finished = false
this.queryPodcasts(this.page)
},
methods: {
...mapActions([
'pauseEpisode',
'playEpisode',
'queryEpisodes',
'clearEpisodes',
]),
doExport() {
window.location
= 'export?requesttoken='
+ encodeURIComponent(getRequestToken())
}, },
async queryPodcasts(page) {
const shows = await showApiClient.queryShows(page)
this.shows = this.shows.concat(shows.data)
},
async loadEpisodes(page) {
const response = await this.queryEpisodes({ page, sortBy: 'pubdate' })
if (response) {
this.page += 1
} else {
this.page = null
}
},
doPlay(episode) {
if (this.episodePlaying(episode.id)) {
this.pauseEpisode()
} else {
this.playEpisode(episode)
}
},
}, },
} }
</script> </script>
<style lang="scss">
.mainContent {
padding: 30px;
}
</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>
<Header :title="t('podcast', 'Currently listening')" />
<LoadMore :page="page"
@load-more="loadEpisodes(page)">
<Table
:episodes="getEpisodes"
:extended="true"
@doPlay="doPlay" />
</LoadMore>
</div>
</template>
<script>
import Table from '../components/Table'
import Header from '../components/Header'
import LoadMore from '../components/LoadMore'
import { mapGetters, mapActions } from 'vuex'
import { setBrowserTitle } from '../utils/misc.js'
export default {
name: 'Listening',
components: {
Table,
LoadMore,
Header,
},
data: () => ({
page: 0,
}),
computed: {
...mapGetters([
'episodePlaying',
'getEpisodes',
]),
},
mounted() {
setBrowserTitle(t('podcast', 'Currently listening'))
this.page = 0
this.clearEpisodes()
},
methods: {
...mapActions([
'pauseEpisode',
'playEpisode',
'queryEpisodes',
'clearEpisodes',
]),
async loadEpisodes(page) {
const response = await this.queryEpisodes(page, 'lastplayed')
if (response) {
this.page += 1
} else {
this.page = null
}
},
doPlay(episode) {
if (this.episodePlaying(episode.id)) {
this.pauseEpisode()
} else {
this.playEpisode(episode)
}
},
},
}
</script>
This diff is collapsed.
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
--> -->
<template> <template>
<div class="mainContent"> <div>
<div <div
v-for="mainIndex in 3" v-for="mainIndex in 3"
:key="mainIndex" :key="mainIndex"
......
This diff is collapsed.
This diff is collapsed.
{
"compilerOptions": {
"module": "ES6",
"moduleResolution": "node",
"target": "ES6",
"strictNullChecks": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
},
"include": [
"./src/**/*"
]
}
const { merge } = require('webpack-merge')
const path = require('path')
const webpackConfig = require('@nextcloud/webpack-vue-config') const webpackConfig = require('@nextcloud/webpack-vue-config')
const path = require('path')
const { merge } = require('webpack-merge')
const config = { const config = {
entry: { entry: {
...@@ -10,20 +11,27 @@ const config = { ...@@ -10,20 +11,27 @@ const config = {
rules: [ rules: [
{ {
test: /\.css$/, test: /\.css$/,
use: ['vue-style-loader', 'css-loader', 'postcss-loader'], use: [
'style-loader',
'css-loader',
],
}, },
{ {
test: /\.scss$/, test: /\.scss$/,
use: [ use: [
'vue-style-loader', 'style-loader',
'css-loader', 'css-loader',
'postcss-loader',
'sass-loader', 'sass-loader',
], ],
}, },
{ {
test: /\.vue$/, test: /\.vue$/,
use: ['vue-loader'], use: 'vue-loader',
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules(?!(\/|\\)(@ckeditor|@nextcloud\/calendar-js|js-base64|)(\/|\\))/
}, },
], ],
}, },
......