Merge pull request 'new homepage based on favorites (fixes #130 #59)' (#131) from favorites into main
All checks were successful
repod / xml (push) Successful in 11s
repod / php (push) Successful in 59s
repod / nodejs (push) Successful in 1m10s
repod / release (push) Successful in 1m37s

Reviewed-on: #131
This commit is contained in:
Michel Roux 2024-09-02 09:27:56 +00:00
commit 094b7812cd
44 changed files with 2010 additions and 764 deletions

View File

@ -1,3 +1,18 @@
## 3.1.0 - 2024-09-02
### Added
- ⭐ You can now add favorites subscriptions !
It will show's up on the homepage instead of the recommendations witch appear only when you add a new subscription.
[#59](https://git.crystalyx.net/Xefir/repod/issues/59) suggested by @W_LL_M, @Jaunty and @Satalink
### Changed
- 💥 Use html5 routing instead of hashes. All the URLs has changed removing the `#/` part.
### Fixed
- 🐛 Regression on 3.0 that prevent seeking player to episode last listened position
[#136](https://git.crystalyx.net/Xefir/repod/issues/136) reported by @randomuser1967
- ⚡ Improve the detection off mis-installed or mis-enabled gpodder app
## 3.0.0 - 2024-08-17
### Added

View File

@ -11,7 +11,7 @@
## Requirements
You need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installed to use this app!]]></description>
<version>3.0.0</version>
<version>3.1.0</version>
<licence>agpl</licence>
<author mail="xefir@crystalyx.net" homepage="https://crystalyx.net">Michel Roux</author>
<namespace>RePod</namespace>

View File

@ -13,6 +13,8 @@ declare(strict_types=1);
return [
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'page#feed', 'url' => '/feed/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+']],
['name' => 'page#discover', 'url' => '/discover', 'verb' => 'GET'],
['name' => 'episodes#action', 'url' => '/episodes/action', 'verb' => 'GET'],
['name' => 'episodes#list', 'url' => '/episodes/list', 'verb' => 'GET'],
['name' => 'opml#export', 'url' => '/opml/export', 'verb' => 'GET'],

View File

@ -15,9 +15,9 @@
"psalm": "psalm --threads=1 --no-cache --show-info=true"
},
"require-dev": {
"nextcloud/ocp": "^29.0.4",
"nextcloud/ocp": "^29.0.5",
"roave/security-advisories": "dev-latest",
"nextcloud/coding-standard": "^1.2.1",
"nextcloud/coding-standard": "^1.2.3",
"vimeo/psalm": "^5.25.0"
},
"config": {

30
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "855f98aa313f86222776e061139c42ec",
"content-hash": "a59841dc91b50fc36ec116bab55543b0",
"packages": [],
"packages-dev": [
{
@ -169,26 +169,26 @@
},
{
"name": "composer/pcre",
"version": "3.2.0",
"version": "3.3.0",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90"
"reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90",
"reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90",
"url": "https://api.github.com/repos/composer/pcre/zipball/1637e067347a0c40bbb1e3cd786b20dcab556a81",
"reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.8"
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.11.8",
"phpstan/phpstan": "^1.11.10",
"phpstan/phpstan-strict-rules": "^1.1",
"phpunit/phpunit": "^8 || ^9"
},
@ -228,7 +228,7 @@
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.2.0"
"source": "https://github.com/composer/pcre/tree/3.3.0"
},
"funding": [
{
@ -244,7 +244,7 @@
"type": "tidelift"
}
],
"time": "2024-07-25T09:36:02+00:00"
"time": "2024-08-19T19:43:53+00:00"
},
{
"name": "composer/semver",
@ -1312,12 +1312,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "251a4f1fefcc6e6cc90d50514fee6b6e3745cb3e"
"reference": "f8de2a81061775002d96aea80b12f2ab3c5eeb8d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/251a4f1fefcc6e6cc90d50514fee6b6e3745cb3e",
"reference": "251a4f1fefcc6e6cc90d50514fee6b6e3745cb3e",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/f8de2a81061775002d96aea80b12f2ab3c5eeb8d",
"reference": "f8de2a81061775002d96aea80b12f2ab3c5eeb8d",
"shasum": ""
},
"conflict": {
@ -1355,7 +1355,7 @@
"athlon1600/php-proxy-app": "<=3",
"austintoddj/canvas": "<=3.4.2",
"auth0/wordpress": "<=4.6",
"automad/automad": "<=2.0.0.0-alpha5",
"automad/automad": "<2.0.0.0-alpha5",
"automattic/jetpack": "<9.8",
"awesome-support/awesome-support": "<=6.0.7",
"aws/aws-sdk-php": "<3.288.1",
@ -1528,7 +1528,7 @@
"friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
"friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6",
"froala/wysiwyg-editor": "<3.2.7|>=4.0.1,<=4.1.3",
"froxlor/froxlor": "<2.1.9",
"froxlor/froxlor": "<=2.2.0.0-RC3",
"frozennode/administrator": "<=5.0.12",
"fuel/core": "<1.8.1",
"funadmin/funadmin": "<=3.2|>=3.3.2,<=3.3.3",
@ -2121,7 +2121,7 @@
"type": "tidelift"
}
],
"time": "2024-08-14T19:05:08+00:00"
"time": "2024-08-23T19:04:38+00:00"
},
{
"name": "sebastian/diff",

View File

@ -18,11 +18,10 @@ OC.L10N.register(
"Link copied to the clipboard" : "Der Link des Feeds wurde in die Zwischenablage kopiert",
"Play" : "Abspielen",
"Stop" : "Stopp",
"Mark as read" : "Als gelesen markieren",
"Mark as unread" : "Als ungelesen markieren",
"Read" : "Gelesen",
"Open website" : "Webseite aufrufen",
"Could not fetch episodes" : "Folgen können nicht abgerufen werden",
"Could not change the status of the episode" : "Kann den Status der Folge nicht ändern",
"Could not fetch episodes" : "Folgen können nicht abgerufen werden",
"Export subscriptions" : "Abonnements exportieren",
"Filtering episodes" : "Folgen filtern",
"Show all" : "Zeige alles",
@ -33,13 +32,17 @@ OC.L10N.register(
"Import OPML file" : "Importiere OPML-Datei",
"Rate RePod ❤️" : "Bewerte RePod ❤️",
"Playback speed" : "Wiedergabegeschwindigkeit",
"Favorite" : "Favorit",
"Are you sure you want to delete this subscription?" : "Bist Du sicher, dass Du das Abonnement löschen möchtest?",
"Error while removing the feed" : "Fehler beim Löschen des Feeds",
"You can only have 10 favorites" : "Du kannst nur 10 Favoriten haben",
"Add a podcast" : "Einen Podcast hinzufügen",
"Could not fetch subscriptions" : "Abonnements können nicht abgerufen werden",
"Find a podcast" : "Finde einen Podcast",
"Error loading feed" : "Fehler beim Laden des Feeds",
"Missing required app" : "Benötigte App fehlt",
"Install GPodder Sync" : "Installiere GPodder Sync"
"Install GPodder Sync" : "Installiere GPodder Sync",
"Pin some subscriptions to see their latest updates" : "Pinne einige Abonnements, um ihre neuesten Updates zu sehen",
"No favorites" : "Keine Favoriten"
},
"");

View File

@ -16,11 +16,10 @@
"Link copied to the clipboard" : "Der Link des Feeds wurde in die Zwischenablage kopiert",
"Play" : "Abspielen",
"Stop" : "Stopp",
"Mark as read" : "Als gelesen markieren",
"Mark as unread" : "Als ungelesen markieren",
"Read" : "Gelesen",
"Open website" : "Webseite aufrufen",
"Could not fetch episodes" : "Folgen können nicht abgerufen werden",
"Could not change the status of the episode" : "Kann den Status der Folge nicht ändern",
"Could not fetch episodes" : "Folgen können nicht abgerufen werden",
"Export subscriptions" : "Abonnements exportieren",
"Filtering episodes" : "Folgen filtern",
"Show all" : "Zeige alles",
@ -31,13 +30,17 @@
"Import OPML file" : "Importiere OPML-Datei",
"Rate RePod ❤️" : "Bewerte RePod ❤️",
"Playback speed" : "Wiedergabegeschwindigkeit",
"Favorite" : "Favorit",
"Are you sure you want to delete this subscription?" : "Bist Du sicher, dass Du das Abonnement löschen möchtest?",
"Error while removing the feed" : "Fehler beim Löschen des Feeds",
"You can only have 10 favorites" : "Du kannst nur 10 Favoriten haben",
"Add a podcast" : "Einen Podcast hinzufügen",
"Could not fetch subscriptions" : "Abonnements können nicht abgerufen werden",
"Find a podcast" : "Finde einen Podcast",
"Error loading feed" : "Fehler beim Laden des Feeds",
"Missing required app" : "Benötigte App fehlt",
"Install GPodder Sync" : "Installiere GPodder Sync"
"Install GPodder Sync" : "Installiere GPodder Sync",
"Pin some subscriptions to see their latest updates" : "Pinne einige Abonnements, um ihre neuesten Updates zu sehen",
"No favorites" : "Keine Favoriten"
},"pluralForm" :""
}

View File

@ -18,11 +18,10 @@ OC.L10N.register(
"Link copied to the clipboard" : "Lien vers le flux copié dans le presse-papiers",
"Play" : "Lecture",
"Stop" : "Arrêter",
"Mark as read" : "Marquer comme lu",
"Mark as unread" : "Marquer comme non lu",
"Read" : "Lu",
"Open website" : "Ouvrir le site web",
"Could not fetch episodes" : "Impossible de récuprer les épisodes",
"Could not change the status of the episode" : "Impossible de changer le status de l'épisode",
"Could not fetch episodes" : "Impossible de récuprer les épisodes",
"Export subscriptions" : "Exporter les abonnements",
"Filtering episodes" : "Filtrage des épisodes",
"Show all" : "Montrer tout",
@ -33,13 +32,17 @@ OC.L10N.register(
"Import OPML file" : "Importer un fichier OPML",
"Rate RePod ❤️" : "Donnez votre avis ❤️",
"Playback speed" : "Vitesse de lecture",
"Favorite" : "Favori",
"Are you sure you want to delete this subscription?" : "Êtes-vous sûr de vouloir supprimer ce flux ?",
"Error while removing the feed" : "Erreur lors de la suppression du flux",
"You can only have 10 favorites" : "Vous ne pouvez avoir que 10 favoris",
"Add a podcast" : "Ajouter un podcast",
"Could not fetch subscriptions" : "Impossible de récupérer les flux",
"Find a podcast" : "Chercher un podcast",
"Error loading feed" : "Erreur lors du chargement du flux",
"Missing required app" : "Une application requise est manquante",
"Install GPodder Sync" : "Installer GPodder Sync"
"Install GPodder Sync" : "Installer GPodder Sync",
"Pin some subscriptions to see their latest updates" : "Ajoutez des abonnements en favoris pour obtenir les dernières nouvelles ici",
"No favorites" : "Aucun favoris"
},
"");

View File

@ -16,11 +16,10 @@
"Link copied to the clipboard" : "Lien vers le flux copié dans le presse-papiers",
"Play" : "Lecture",
"Stop" : "Arrêter",
"Mark as read" : "Marquer comme lu",
"Mark as unread" : "Marquer comme non lu",
"Read" : "Lu",
"Open website" : "Ouvrir le site web",
"Could not fetch episodes" : "Impossible de récuprer les épisodes",
"Could not change the status of the episode" : "Impossible de changer le status de l'épisode",
"Could not fetch episodes" : "Impossible de récuprer les épisodes",
"Export subscriptions" : "Exporter les abonnements",
"Filtering episodes" : "Filtrage des épisodes",
"Show all" : "Montrer tout",
@ -31,13 +30,17 @@
"Import OPML file" : "Importer un fichier OPML",
"Rate RePod ❤️" : "Donnez votre avis ❤️",
"Playback speed" : "Vitesse de lecture",
"Favorite" : "Favori",
"Are you sure you want to delete this subscription?" : "Êtes-vous sûr de vouloir supprimer ce flux ?",
"Error while removing the feed" : "Erreur lors de la suppression du flux",
"You can only have 10 favorites" : "Vous ne pouvez avoir que 10 favoris",
"Add a podcast" : "Ajouter un podcast",
"Could not fetch subscriptions" : "Impossible de récupérer les flux",
"Find a podcast" : "Chercher un podcast",
"Error loading feed" : "Erreur lors du chargement du flux",
"Missing required app" : "Une application requise est manquante",
"Install GPodder Sync" : "Installer GPodder Sync"
"Install GPodder Sync" : "Installer GPodder Sync",
"Pin some subscriptions to see their latest updates" : "Ajoutez des abonnements en favoris pour obtenir les dernières nouvelles ici",
"No favorites" : "Aucun favoris"
},"pluralForm" :""
}

View File

@ -32,6 +32,10 @@ class Application extends App implements IBootstrap
/** @var IInitialState $initialState */
$initialState = $appContainer->get(IInitialState::class);
if (null === $appManager->getAppInfo(self::GPODDERSYNC_ID)) {
$appManager->disableApp(self::GPODDERSYNC_ID);
}
$gpoddersync = $appManager->isEnabledForUser(self::GPODDERSYNC_ID);
if (!$gpoddersync) {
try {

View File

@ -8,10 +8,19 @@ use OCA\RePod\AppInfo\Application;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\IRequest;
use OCP\Util;
class PageController extends Controller
{
public function __construct(
IRequest $request,
private IConfig $config
) {
parent::__construct(Application::APP_ID, $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
@ -23,9 +32,32 @@ class PageController extends Controller
$csp->addAllowedImageDomain('*');
$csp->addAllowedMediaDomain('*');
if ($this->config->getSystemValueBool('debug', false)) {
/** @psalm-suppress DeprecatedMethod */
$csp->allowEvalScript();
$csp->addAllowedConnectDomain('*');
$csp->addAllowedScriptDomain('*');
}
$response = new TemplateResponse(Application::APP_ID, 'main');
$response->setContentSecurityPolicy($csp);
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function discover(): TemplateResponse {
return $this->index();
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function feed(): TemplateResponse {
return $this->index();
}
}

1494
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@
"linkify-html": "^4.1.3",
"pinia": "^2.2.2",
"toastify-js": "^1.12.0",
"vite": "^5.4.1",
"vite": "^5.4.2",
"vue": "^3.4.38",
"vue-material-design-icons": "^5.3.0",
"vue-router": "^4.4.3"
@ -37,7 +37,8 @@
"@nextcloud/prettier-config": "^1.1.0",
"@nextcloud/stylelint-config": "^3.0.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-pinia": "^0.4.0",
"eslint-plugin-prettier": "^5.2.1"
"eslint-plugin-pinia": "^0.4.1",
"eslint-plugin-prettier": "^5.2.1",
"vite-plugin-vue-devtools": "^7.3.9"
}
}

View File

@ -1,5 +1,5 @@
<template>
<NcAppContent :class="{ padding: episode }">
<NcAppContent :class="{ episode }">
<slot />
</NcAppContent>
</template>
@ -21,7 +21,7 @@ export default {
</script>
<style scoped>
.padding {
.episode {
padding-bottom: 6rem;
}
</style>

View File

@ -0,0 +1,22 @@
<template>
<NcEmptyContent class="empty">
<slot />
</NcEmptyContent>
</template>
<script>
import { NcEmptyContent } from '@nextcloud/vue'
export default {
name: 'EmptyContent',
components: {
NcEmptyContent,
},
}
</script>
<style scoped>
.empty {
height: 100%;
}
</style>

View File

@ -1,25 +1,29 @@
<template>
<div class="flex">
<NcAvatar :display-name="name" :is-no-user="true" :size="256" :url="image" />
<h2>{{ name }}</h2>
<SafeHtml :source="description" />
<NcAvatar
:display-name="episode.name"
:is-no-user="true"
:size="256"
:url="episode.image" />
<h2>{{ episode.name }}</h2>
<SafeHtml :source="episode.description" />
<div class="flex">
<NcButton v-if="link" :href="link" target="_blank">
<NcButton v-if="episode.link" :href="episode.link" target="_blank">
<template #icon>
<OpenInNewIcon :size="20" />
</template>
{{ title }}
{{ episode.title }}
</NcButton>
<NcButton
v-if="url"
:download="filenameFromUrl(url)"
:href="url"
v-if="episode.url"
:download="filenameFromUrl(episode.url)"
:href="episode.url"
target="_blank">
<template #icon>
<DownloadIcon :size="20" />
</template>
{{ t('repod', 'Download') }}
{{ size ? `(${humanFileSize(size)})` : '' }}
{{ episode.size ? `(${humanFileSize(episode.size)})` : '' }}
</NcButton>
</div>
</div>
@ -43,32 +47,8 @@ export default {
SafeHtml,
},
props: {
description: {
type: String,
default: '',
},
image: {
type: String,
required: true,
},
link: {
type: String,
default: null,
},
name: {
type: String,
required: true,
},
size: {
type: Number,
default: null,
},
title: {
type: String,
required: true,
},
url: {
type: String,
episode: {
type: Object,
required: true,
},
},

View File

@ -2,7 +2,7 @@
<NcAppNavigationList>
<NcAppNavigationNewItem
:name="t('repod', 'Add a RSS link')"
@new-item="addSubscription">
@new-item="(url) => $router.push(toFeedUrl(url))">
<template #icon>
<PlusIcon :size="20" />
</template>
@ -13,7 +13,7 @@
<script>
import { NcAppNavigationList, NcAppNavigationNewItem } from '@nextcloud/vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import { encodeUrl } from '../../utils/url.js'
import { toFeedUrl } from '../../utils/url.js'
export default {
name: 'AddRss',
@ -23,9 +23,7 @@ export default {
PlusIcon,
},
methods: {
addSubscription(feedUrl) {
this.$router.push(encodeUrl(feedUrl))
},
toFeedUrl,
},
}
</script>

View File

@ -7,7 +7,7 @@
:key="feed.link"
:details="formatLocaleDate(new Date(feed.fetchedAtUnix * 1000))"
:name="feed.title"
:to="toUrl(feed.link)">
:to="toFeedUrl(feed.link)">
<template #icon>
<NcAvatar
:display-name="feed.author"
@ -19,7 +19,7 @@
</template>
<template #actions>
<NcActionButton
v-if="!subscriptions.includes(feed.link)"
v-if="!getSubscriptions.includes(feed.link)"
:aria-label="t('repod', 'Subscribe')"
:name="t('repod', 'Subscribe')"
:title="t('repod', 'Subscribe')"
@ -44,7 +44,7 @@ import { debounce } from '../../utils/debounce.js'
import { formatLocaleDate } from '../../utils/time.js'
import { generateUrl } from '@nextcloud/router'
import { showError } from '../../utils/toast.js'
import { toUrl } from '../../utils/url.js'
import { toFeedUrl } from '../../utils/url.js'
import { useSubscriptions } from '../../store/subscriptions.js'
export default {
@ -62,14 +62,12 @@ export default {
required: true,
},
},
data() {
return {
feeds: [],
loading: false,
}
},
data: () => ({
feeds: [],
loading: false,
}),
computed: {
...mapState(useSubscriptions, ['subscriptions']),
...mapState(useSubscriptions, ['getSubscriptions']),
},
watch: {
value() {
@ -79,7 +77,7 @@ export default {
methods: {
...mapActions(useSubscriptions, ['fetch']),
formatLocaleDate,
toUrl,
toFeedUrl,
async addSubscription(url) {
try {
await axios.post(

View File

@ -4,7 +4,7 @@
<Loading v-if="loading" />
<ul v-if="!loading">
<li v-for="top in tops" :key="top.link">
<router-link :to="toUrl(top.link)">
<router-link :to="toFeedUrl(top.link)">
<img :src="top.imageUrl" :title="top.author" />
</router-link>
</li>
@ -17,7 +17,7 @@ import Loading from '../Atoms/Loading.vue'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { showError } from '../../utils/toast.js'
import { toUrl } from '../../utils/url.js'
import { toFeedUrl } from '../../utils/url.js'
export default {
name: 'Toplist',
@ -30,12 +30,10 @@ export default {
required: true,
},
},
data() {
return {
loading: true,
tops: [],
}
},
data: () => ({
loading: true,
tops: [],
}),
computed: {
title() {
switch (this.type) {
@ -63,7 +61,7 @@ export default {
}
},
methods: {
toUrl,
toFeedUrl,
},
}
</script>

View File

@ -23,7 +23,7 @@
<SafeHtml :source="description" />
</div>
<NcAppNavigationNew
v-if="!subscriptions.includes(url)"
v-if="!getSubscriptions.includes(url)"
:text="t('repod', 'Subscribe')"
@click="addSubscription">
<template #icon>
@ -79,7 +79,7 @@ export default {
},
},
computed: {
...mapState(useSubscriptions, ['subscriptions']),
...mapState(useSubscriptions, ['getSubscriptions']),
url() {
return decodeUrl(this.$route.params.url)
},

View File

@ -0,0 +1,203 @@
<template>
<NcListItem
:active="isCurrentEpisode(episode)"
:details="!oneLine ? formatLocaleDate(new Date(episode.pubDate?.date)) : ''"
:force-display-actions="true"
:name="episode.name"
:one-line="oneLine"
:style="{ opacity: hasEnded(episode) ? 0.4 : 1 }"
:title="episode.description"
@click="modalEpisode = episode">
<template #actions>
<NcActionButton
v-if="!isCurrentEpisode(episode)"
:aria-label="t('repod', 'Play')"
:title="t('repod', 'Play')"
@click="load(episode, url)">
<template #icon>
<PlayIcon :size="20" />
</template>
</NcActionButton>
<NcActionButton
v-if="isCurrentEpisode(episode)"
:aria-label="t('repod', 'Stop')"
:title="t('repod', 'Stop')"
@click="load(null)">
<template #icon>
<StopIcon :size="20" />
</template>
</NcActionButton>
</template>
<template #extra>
<NcActions>
<NcActionButton
v-if="episode.duration"
:aria-label="t('repod', 'Read')"
:disabled="loading"
:model-value="hasEnded(episode)"
:name="t('repod', 'Read')"
:title="t('repod', 'Read')"
@click="markAs(episode, !hasEnded(episode))">
<template #icon>
<PlaylistPlayIcon v-if="!hasEnded(episode)" :size="20" />
<PlaylistRemoveIcon v-if="hasEnded(episode)" :size="20" />
</template>
</NcActionButton>
<NcActionLink
v-if="episode.link"
:href="episode.link"
:name="t('repod', 'Open website')"
target="_blank"
:title="t('repod', 'Open website')">
<template #icon>
<OpenInNewIcon :size="20" />
</template>
</NcActionLink>
<NcActionLink
v-if="episode.url"
:download="filenameFromUrl(episode.url)"
:href="episode.url"
:name="t('repod', 'Download')"
target="_blank"
:title="t('repod', 'Download')">
<template #icon>
<DownloadIcon :size="20" />
</template>
</NcActionLink>
</NcActions>
<NcModal v-if="modalEpisode" @close="modalEpisode = null">
<Modal :episode="episode" />
</NcModal>
</template>
<template #icon>
<NcAvatar
:display-name="episode.name"
:is-no-user="true"
:url="episode.image" />
</template>
<template #indicator>
<NcProgressBar
v-if="isListening(episode) && !oneLine"
class="progress"
:value="(episode.action.position * 100) / episode.action.total" />
</template>
<template v-if="!oneLine" #subname>
{{ episode.duration }}
</template>
</NcListItem>
</template>
<script>
import {
NcActionButton,
NcActionLink,
NcActions,
NcAvatar,
NcListItem,
NcModal,
NcProgressBar,
} from '@nextcloud/vue'
import {
durationToSeconds,
formatEpisodeTimestamp,
formatLocaleDate,
} from '../../utils/time.js'
import { hasEnded, isListening } from '../../utils/status.js'
import { mapActions, mapState } from 'pinia'
import DownloadIcon from 'vue-material-design-icons/Download.vue'
import Modal from '../Atoms/Modal.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import PlayIcon from 'vue-material-design-icons/Play.vue'
import PlaylistPlayIcon from 'vue-material-design-icons/PlaylistPlay.vue'
import PlaylistRemoveIcon from 'vue-material-design-icons/PlaylistRemove.vue'
import StopIcon from 'vue-material-design-icons/Stop.vue'
import axios from '@nextcloud/axios'
import { filenameFromUrl } from '../../utils/url.js'
import { generateUrl } from '@nextcloud/router'
import { showError } from '../../utils/toast.js'
import { usePlayer } from '../../store/player.js'
export default {
name: 'Episode',
components: {
DownloadIcon,
Modal,
NcActionButton,
NcActionLink,
NcActions,
NcAvatar,
NcListItem,
NcModal,
NcProgressBar,
OpenInNewIcon,
PlayIcon,
PlaylistPlayIcon,
PlaylistRemoveIcon,
StopIcon,
},
props: {
episode: {
type: Object,
required: true,
},
oneLine: {
type: Boolean,
default: false,
},
url: {
type: String,
required: true,
},
},
data: () => ({
loading: false,
modalEpisode: null,
}),
computed: {
...mapState(usePlayer, { playerEpisode: 'episode' }),
},
methods: {
...mapActions(usePlayer, ['load']),
formatLocaleDate,
hasEnded,
isListening,
filenameFromUrl,
isCurrentEpisode(episode) {
return this.playerEpisode?.url === episode.url
},
async markAs(episode, read) {
try {
this.loading = true
episode.action = {
podcast: this.url,
episode: episode.url,
guid: episode.guid,
action: 'play',
timestamp: formatEpisodeTimestamp(new Date()),
started: episode.action?.started || 0,
position: read ? durationToSeconds(episode.duration) : 0,
total: durationToSeconds(episode.duration),
}
await axios.post(
generateUrl('/apps/gpoddersync/episode_action/create'),
[episode.action],
)
if (read && this.isCurrentEpisode(episode)) {
this.load(null)
}
} catch (e) {
console.error(e)
showError(t('repod', 'Could not change the status of the episode'))
} finally {
this.loading = false
}
},
},
}
</script>
<style scoped>
.progress {
margin-top: 0.4rem;
}
</style>

View File

@ -2,144 +2,23 @@
<div>
<Loading v-if="loading" />
<ul v-if="!loading">
<NcListItem
<Episode
v-for="episode in filteredEpisodes"
:key="episode.guid"
:active="isCurrentEpisode(episode)"
:details="formatLocaleDate(new Date(episode.pubDate?.date))"
:force-display-actions="true"
:href="$route.href"
:name="episode.name"
:style="{ opacity: hasEnded(episode) ? 0.4 : 1 }"
target="_self"
:title="episode.description"
@click="modalEpisode = episode">
<template #actions>
<NcActionButton
v-if="!isCurrentEpisode(episode)"
:aria-label="t('repod', 'Play')"
:title="t('repod', 'Play')"
@click="load(episode, url)">
<template #icon>
<PlayIcon :size="20" />
</template>
</NcActionButton>
<NcActionButton
v-if="isCurrentEpisode(episode)"
:aria-label="t('repod', 'Stop')"
:title="t('repod', 'Stop')"
@click="load(null)">
<template #icon>
<StopIcon :size="20" />
</template>
</NcActionButton>
</template>
<template #extra>
<NcActions>
<NcActionButton
v-if="episode.duration && !hasEnded(episode)"
:aria-label="t('repod', 'Mark as read')"
:disabled="loadingAction"
:name="t('repod', 'Mark as read')"
:title="t('repod', 'Mark as read')"
@click="markAs(episode, true)">
<template #icon>
<PlaylistPlayIcon :size="20" />
</template>
</NcActionButton>
<NcActionButton
v-if="episode.duration && hasEnded(episode)"
:aria-label="t('repod', 'Mark as unread')"
:disabled="loadingAction"
:name="t('repod', 'Mark as unread')"
:title="t('repod', 'Mark as unread')"
@click="markAs(episode, false)">
<template #icon>
<PlaylistRemoveIcon :size="20" />
</template>
</NcActionButton>
<NcActionLink
v-if="episode.link"
:href="episode.link"
:name="t('repod', 'Open website')"
target="_blank"
:title="t('repod', 'Open website')">
<template #icon>
<OpenInNewIcon :size="20" />
</template>
</NcActionLink>
<NcActionLink
v-if="episode.url"
:download="filenameFromUrl(episode.url)"
:href="episode.url"
:name="t('repod', 'Download')"
target="_blank"
:title="t('repod', 'Download')">
<template #icon>
<DownloadIcon :size="20" />
</template>
</NcActionLink>
</NcActions>
</template>
<template #icon>
<NcAvatar
:display-name="episode.name"
:is-no-user="true"
:url="episode.image" />
</template>
<template #indicator>
<NcProgressBar
v-if="isListening(episode)"
class="progress"
:value="
(episode.action.position * 100) / episode.action.total
" />
</template>
<template #subname>
{{ episode.duration }}
</template>
</NcListItem>
:episode="episode"
:url="url" />
</ul>
<NcModal v-if="modalEpisode" @close="modalEpisode = null">
<Modal
:description="modalEpisode.description"
:image="modalEpisode.image"
:link="modalEpisode.link"
:name="modalEpisode.name"
:size="modalEpisode.size"
:title="modalEpisode.title"
:url="modalEpisode.url" />
</NcModal>
</div>
</template>
<script>
import {
NcActionButton,
NcActionLink,
NcActions,
NcAvatar,
NcListItem,
NcModal,
NcProgressBar,
} from '@nextcloud/vue'
import { decodeUrl, filenameFromUrl } from '../../utils/url.js'
import {
durationToSeconds,
formatEpisodeTimestamp,
formatLocaleDate,
} from '../../utils/time.js'
import { mapActions, mapState } from 'pinia'
import DownloadIcon from 'vue-material-design-icons/Download.vue'
import { hasEnded, isListening } from '../../utils/status.js'
import Episode from './Episode.vue'
import Loading from '../Atoms/Loading.vue'
import Modal from '../Atoms/Modal.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import PlayIcon from 'vue-material-design-icons/Play.vue'
import PlaylistPlayIcon from 'vue-material-design-icons/PlaylistPlay.vue'
import PlaylistRemoveIcon from 'vue-material-design-icons/PlaylistRemove.vue'
import StopIcon from 'vue-material-design-icons/Stop.vue'
import axios from '@nextcloud/axios'
import { decodeUrl } from '../../utils/url.js'
import { generateUrl } from '@nextcloud/router'
import { mapState } from 'pinia'
import { showError } from '../../utils/toast.js'
import { usePlayer } from '../../store/player.js'
import { useSettings } from '../../store/settings.js'
@ -147,30 +26,13 @@ import { useSettings } from '../../store/settings.js'
export default {
name: 'Episodes',
components: {
DownloadIcon,
Episode,
Loading,
Modal,
NcActionButton,
NcActionLink,
NcActions,
NcAvatar,
NcListItem,
NcModal,
NcProgressBar,
OpenInNewIcon,
PlayIcon,
PlaylistPlayIcon,
PlaylistRemoveIcon,
StopIcon,
},
data() {
return {
episodes: [],
loading: true,
loadingAction: false,
modalEpisode: null,
}
},
data: () => ({
episodes: [],
loading: true,
}),
computed: {
...mapState(usePlayer, ['episode']),
...mapState(useSettings, ['filters']),
@ -223,63 +85,8 @@ export default {
}
},
methods: {
...mapActions(usePlayer, ['load']),
filenameFromUrl,
formatLocaleDate,
hasEnded(episode) {
return (
episode.action &&
(episode.action.action === 'DELETE' ||
(episode.action.position > 0 &&
episode.action.total > 0 &&
episode.action.position >= episode.action.total))
)
},
isCurrentEpisode(episode) {
return this.episode && this.episode.url === episode.url
},
isListening(episode) {
return (
episode.action &&
episode.action.action &&
episode.action.action.toLowerCase() === 'play' &&
episode.action.position > 0 &&
!this.hasEnded(episode)
)
},
async markAs(episode, read) {
try {
this.loadingAction = true
episode.action = {
podcast: this.url,
episode: episode.url,
guid: episode.guid,
action: 'play',
timestamp: formatEpisodeTimestamp(new Date()),
started: episode.action ? episode.action.started : 0,
position: read ? durationToSeconds(episode.duration) : 0,
total: durationToSeconds(episode.duration),
}
await axios.post(
generateUrl('/apps/gpoddersync/episode_action/create'),
[episode.action],
)
if (read && this.episode && episode.url === this.episode.url) {
this.load(null)
}
} catch (e) {
console.error(e)
showError(t('repod', 'Could not change the status of the episode'))
} finally {
this.loadingAction = false
}
},
hasEnded,
isListening,
},
}
</script>
<style scoped>
.progress {
margin-top: 0.4rem;
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<NcGuestContent class="guest">
<Loading v-if="!currentFavoriteData" />
<NcAvatar
v-if="currentFavoriteData"
class="avatar"
:display-name="currentFavoriteData.author || currentFavoriteData.title"
:is-no-user="true"
:size="222"
:url="currentFavoriteData.imageUrl" />
<div class="list">
<h2 class="title">{{ currentFavoriteData.title }}</h2>
<Loading v-if="loading" />
<ul v-if="!loading">
<Episode
v-for="episode in episodes"
:key="episode.guid"
:episode="episode"
:one-line="true"
:url="url" />
</ul>
</div>
</NcGuestContent>
</template>
<script>
import { NcAvatar, NcGuestContent } from '@nextcloud/vue'
import Episode from './Episode.vue'
import Loading from '../Atoms/Loading.vue'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { hasEnded } from '../../utils/status.js'
import { mapState } from 'pinia'
import { showError } from '../../utils/toast.js'
import { useSubscriptions } from '../../store/subscriptions.js'
export default {
name: 'Favorites',
components: {
Episode,
Loading,
NcAvatar,
NcGuestContent,
},
props: {
url: {
type: String,
required: true,
},
},
data: () => ({
episodes: [],
loading: true,
}),
computed: {
...mapState(useSubscriptions, ['getFavorites']),
currentFavoriteData() {
return this.getFavorites.find((fav) => fav.url === this.url)
},
},
async mounted() {
try {
this.loading = true
const episodes = await axios.get(
generateUrl('/apps/repod/episodes/list?url={url}', {
url: this.url,
}),
)
this.episodes = [...episodes.data]
.sort(
(a, b) => new Date(b.pubDate?.date) - new Date(a.pubDate?.date),
)
.filter((episode) => !this.hasEnded(episode))
.slice(0, 4)
} catch (e) {
console.error(e)
showError(t('repod', 'Could not fetch episodes'))
} finally {
this.loading = false
}
},
methods: {
hasEnded,
},
}
</script>
<style scoped>
.guest {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.list {
flex: 1;
}
.title {
text-align: center;
}
@media only screen and (max-width: 768px) {
.avatar {
display: none;
}
}
</style>

View File

@ -78,7 +78,7 @@ export default {
@media only screen and (max-width: 768px) {
.infos {
flex: 2;
flex: 1;
}
.timer,

View File

@ -3,18 +3,11 @@
<strong class="pointer" @click="modal = true">
{{ episode.name }}
</strong>
<router-link :to="hash">
<router-link :to="toFeedUrl(podcastUrl)">
<i>{{ episode.title }}</i>
</router-link>
<NcModal v-if="modal" @close="modal = false">
<Modal
:description="episode.description"
:image="episode.image"
:link="episode.link"
:name="episode.name"
:size="episode.size"
:title="episode.title"
:url="episode.url" />
<Modal :episode="episode" />
</NcModal>
</div>
</template>
@ -23,7 +16,7 @@
import Modal from '../Atoms/Modal.vue'
import { NcModal } from '@nextcloud/vue'
import { mapState } from 'pinia'
import { toUrl } from '../../utils/url.js'
import { toFeedUrl } from '../../utils/url.js'
import { usePlayer } from '../../store/player.js'
export default {
@ -32,16 +25,14 @@ export default {
Modal,
NcModal,
},
data() {
return {
modal: false,
}
},
data: () => ({
modal: false,
}),
computed: {
...mapState(usePlayer, ['episode', 'podcastUrl']),
hash() {
return toUrl(this.podcastUrl)
},
},
methods: {
toFeedUrl,
},
}
</script>

View File

@ -46,11 +46,9 @@ export default {
VolumeMediumIcon,
VolumeMuteIcon,
},
data() {
return {
volumeMuted: 0,
}
},
data: () => ({
volumeMuted: 0,
}),
computed: {
...mapState(usePlayer, ['volume']),
},

View File

@ -44,7 +44,6 @@ import { NcActionCheckbox, NcAppNavigationItem } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia'
import FilterIcon from 'vue-material-design-icons/Filter.vue'
import FilterSettingsIcon from 'vue-material-design-icons/FilterSettings.vue'
import { getCookie } from '../../utils/cookies.js'
import { useSettings } from '../../store/settings.js'
export default {
@ -65,12 +64,6 @@ export default {
)
},
},
mounted() {
try {
const filters = getCookie('repod.filters')
this.filters = JSON.parse(filters)
} catch {}
},
methods: {
...mapActions(useSettings, ['setFilters']),
},

View File

@ -44,12 +44,10 @@ export default {