new homepage based on favorites (fixes #130 #59) #131

Merged
Xefir merged 23 commits from favorites into main 2024-09-02 09:28:09 +00:00
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 ## 3.0.0 - 2024-08-17
### Added ### Added

View File

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

View File

@ -13,6 +13,8 @@ declare(strict_types=1);
return [ return [
'routes' => [ 'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], ['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#action', 'url' => '/episodes/action', 'verb' => 'GET'],
['name' => 'episodes#list', 'url' => '/episodes/list', 'verb' => 'GET'], ['name' => 'episodes#list', 'url' => '/episodes/list', 'verb' => 'GET'],
['name' => 'opml#export', 'url' => '/opml/export', '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" "psalm": "psalm --threads=1 --no-cache --show-info=true"
}, },
"require-dev": { "require-dev": {
"nextcloud/ocp": "^29.0.4", "nextcloud/ocp": "^29.0.5",
"roave/security-advisories": "dev-latest", "roave/security-advisories": "dev-latest",
"nextcloud/coding-standard": "^1.2.1", "nextcloud/coding-standard": "^1.2.3",
"vimeo/psalm": "^5.25.0" "vimeo/psalm": "^5.25.0"
}, },
"config": { "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "855f98aa313f86222776e061139c42ec", "content-hash": "a59841dc91b50fc36ec116bab55543b0",
"packages": [], "packages": [],
"packages-dev": [ "packages-dev": [
{ {
@ -169,26 +169,26 @@
}, },
{ {
"name": "composer/pcre", "name": "composer/pcre",
"version": "3.2.0", "version": "3.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/pcre.git", "url": "https://github.com/composer/pcre.git",
"reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" "reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", "url": "https://api.github.com/repos/composer/pcre/zipball/1637e067347a0c40bbb1e3cd786b20dcab556a81",
"reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", "reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.4 || ^8.0" "php": "^7.4 || ^8.0"
}, },
"conflict": { "conflict": {
"phpstan/phpstan": "<1.11.8" "phpstan/phpstan": "<1.11.10"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.11.8", "phpstan/phpstan": "^1.11.10",
"phpstan/phpstan-strict-rules": "^1.1", "phpstan/phpstan-strict-rules": "^1.1",
"phpunit/phpunit": "^8 || ^9" "phpunit/phpunit": "^8 || ^9"
}, },
@ -228,7 +228,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/composer/pcre/issues", "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": [ "funding": [
{ {
@ -244,7 +244,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-07-25T09:36:02+00:00" "time": "2024-08-19T19:43:53+00:00"
}, },
{ {
"name": "composer/semver", "name": "composer/semver",
@ -1312,12 +1312,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git", "url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "251a4f1fefcc6e6cc90d50514fee6b6e3745cb3e" "reference": "f8de2a81061775002d96aea80b12f2ab3c5eeb8d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/251a4f1fefcc6e6cc90d50514fee6b6e3745cb3e", "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/f8de2a81061775002d96aea80b12f2ab3c5eeb8d",
"reference": "251a4f1fefcc6e6cc90d50514fee6b6e3745cb3e", "reference": "f8de2a81061775002d96aea80b12f2ab3c5eeb8d",
"shasum": "" "shasum": ""
}, },
"conflict": { "conflict": {
@ -1355,7 +1355,7 @@
"athlon1600/php-proxy-app": "<=3", "athlon1600/php-proxy-app": "<=3",
"austintoddj/canvas": "<=3.4.2", "austintoddj/canvas": "<=3.4.2",
"auth0/wordpress": "<=4.6", "auth0/wordpress": "<=4.6",
"automad/automad": "<=2.0.0.0-alpha5", "automad/automad": "<2.0.0.0-alpha5",
"automattic/jetpack": "<9.8", "automattic/jetpack": "<9.8",
"awesome-support/awesome-support": "<=6.0.7", "awesome-support/awesome-support": "<=6.0.7",
"aws/aws-sdk-php": "<3.288.1", "aws/aws-sdk-php": "<3.288.1",
@ -1528,7 +1528,7 @@
"friendsoftypo3/mediace": ">=7.6.2,<7.6.5", "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", "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", "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", "frozennode/administrator": "<=5.0.12",
"fuel/core": "<1.8.1", "fuel/core": "<1.8.1",
"funadmin/funadmin": "<=3.2|>=3.3.2,<=3.3.3", "funadmin/funadmin": "<=3.2|>=3.3.2,<=3.3.3",
@ -2121,7 +2121,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-08-14T19:05:08+00:00" "time": "2024-08-23T19:04:38+00:00"
}, },
{ {
"name": "sebastian/diff", "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", "Link copied to the clipboard" : "Der Link des Feeds wurde in die Zwischenablage kopiert",
"Play" : "Abspielen", "Play" : "Abspielen",
"Stop" : "Stopp", "Stop" : "Stopp",
"Mark as read" : "Als gelesen markieren", "Read" : "Gelesen",
"Mark as unread" : "Als ungelesen markieren",
"Open website" : "Webseite aufrufen", "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 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", "Export subscriptions" : "Abonnements exportieren",
"Filtering episodes" : "Folgen filtern", "Filtering episodes" : "Folgen filtern",
"Show all" : "Zeige alles", "Show all" : "Zeige alles",
@ -33,13 +32,17 @@ OC.L10N.register(
"Import OPML file" : "Importiere OPML-Datei", "Import OPML file" : "Importiere OPML-Datei",
"Rate RePod ❤️" : "Bewerte RePod ❤️", "Rate RePod ❤️" : "Bewerte RePod ❤️",
"Playback speed" : "Wiedergabegeschwindigkeit", "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?", "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", "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", "Add a podcast" : "Einen Podcast hinzufügen",
"Could not fetch subscriptions" : "Abonnements können nicht abgerufen werden", "Could not fetch subscriptions" : "Abonnements können nicht abgerufen werden",
"Find a podcast" : "Finde einen Podcast", "Find a podcast" : "Finde einen Podcast",
"Error loading feed" : "Fehler beim Laden des Feeds", "Error loading feed" : "Fehler beim Laden des Feeds",
"Missing required app" : "Benötigte App fehlt", "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", "Link copied to the clipboard" : "Der Link des Feeds wurde in die Zwischenablage kopiert",
"Play" : "Abspielen", "Play" : "Abspielen",
"Stop" : "Stopp", "Stop" : "Stopp",
"Mark as read" : "Als gelesen markieren", "Read" : "Gelesen",
"Mark as unread" : "Als ungelesen markieren",
"Open website" : "Webseite aufrufen", "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 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", "Export subscriptions" : "Abonnements exportieren",
"Filtering episodes" : "Folgen filtern", "Filtering episodes" : "Folgen filtern",
"Show all" : "Zeige alles", "Show all" : "Zeige alles",
@ -31,13 +30,17 @@
"Import OPML file" : "Importiere OPML-Datei", "Import OPML file" : "Importiere OPML-Datei",
"Rate RePod ❤️" : "Bewerte RePod ❤️", "Rate RePod ❤️" : "Bewerte RePod ❤️",
"Playback speed" : "Wiedergabegeschwindigkeit", "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?", "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", "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", "Add a podcast" : "Einen Podcast hinzufügen",
"Could not fetch subscriptions" : "Abonnements können nicht abgerufen werden", "Could not fetch subscriptions" : "Abonnements können nicht abgerufen werden",
"Find a podcast" : "Finde einen Podcast", "Find a podcast" : "Finde einen Podcast",
"Error loading feed" : "Fehler beim Laden des Feeds", "Error loading feed" : "Fehler beim Laden des Feeds",
"Missing required app" : "Benötigte App fehlt", "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" :"" },"pluralForm" :""
} }

View File

@ -18,11 +18,10 @@ OC.L10N.register(
"Link copied to the clipboard" : "Lien vers le flux copié dans le presse-papiers", "Link copied to the clipboard" : "Lien vers le flux copié dans le presse-papiers",
"Play" : "Lecture", "Play" : "Lecture",
"Stop" : "Arrêter", "Stop" : "Arrêter",
"Mark as read" : "Marquer comme lu", "Read" : "Lu",
"Mark as unread" : "Marquer comme non lu",
"Open website" : "Ouvrir le site web", "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 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", "Export subscriptions" : "Exporter les abonnements",
"Filtering episodes" : "Filtrage des épisodes", "Filtering episodes" : "Filtrage des épisodes",
"Show all" : "Montrer tout", "Show all" : "Montrer tout",
@ -33,13 +32,17 @@ OC.L10N.register(
"Import OPML file" : "Importer un fichier OPML", "Import OPML file" : "Importer un fichier OPML",
"Rate RePod ❤️" : "Donnez votre avis ❤️", "Rate RePod ❤️" : "Donnez votre avis ❤️",
"Playback speed" : "Vitesse de lecture", "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 ?", "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", "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", "Add a podcast" : "Ajouter un podcast",
"Could not fetch subscriptions" : "Impossible de récupérer les flux", "Could not fetch subscriptions" : "Impossible de récupérer les flux",
"Find a podcast" : "Chercher un podcast", "Find a podcast" : "Chercher un podcast",
"Error loading feed" : "Erreur lors du chargement du flux", "Error loading feed" : "Erreur lors du chargement du flux",
"Missing required app" : "Une application requise est manquante", "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", "Link copied to the clipboard" : "Lien vers le flux copié dans le presse-papiers",
"Play" : "Lecture", "Play" : "Lecture",
"Stop" : "Arrêter", "Stop" : "Arrêter",
"Mark as read" : "Marquer comme lu", "Read" : "Lu",
"Mark as unread" : "Marquer comme non lu",
"Open website" : "Ouvrir le site web", "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 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", "Export subscriptions" : "Exporter les abonnements",
"Filtering episodes" : "Filtrage des épisodes", "Filtering episodes" : "Filtrage des épisodes",
"Show all" : "Montrer tout", "Show all" : "Montrer tout",
@ -31,13 +30,17 @@
"Import OPML file" : "Importer un fichier OPML", "Import OPML file" : "Importer un fichier OPML",
"Rate RePod ❤️" : "Donnez votre avis ❤️", "Rate RePod ❤️" : "Donnez votre avis ❤️",
"Playback speed" : "Vitesse de lecture", "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 ?", "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", "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", "Add a podcast" : "Ajouter un podcast",
"Could not fetch subscriptions" : "Impossible de récupérer les flux", "Could not fetch subscriptions" : "Impossible de récupérer les flux",
"Find a podcast" : "Chercher un podcast", "Find a podcast" : "Chercher un podcast",
"Error loading feed" : "Erreur lors du chargement du flux", "Error loading feed" : "Erreur lors du chargement du flux",
"Missing required app" : "Une application requise est manquante", "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" :"" },"pluralForm" :""
} }

View File

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

View File

@ -8,10 +8,19 @@ use OCA\RePod\AppInfo\Application;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\IRequest;
use OCP\Util; use OCP\Util;
class PageController extends Controller class PageController extends Controller
{ {
public function __construct(
IRequest $request,
private IConfig $config
) {
parent::__construct(Application::APP_ID, $request);
}
/** /**
* @NoAdminRequired * @NoAdminRequired
* @NoCSRFRequired * @NoCSRFRequired
@ -23,9 +32,32 @@ class PageController extends Controller
$csp->addAllowedImageDomain('*'); $csp->addAllowedImageDomain('*');
$csp->addAllowedMediaDomain('*'); $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 = new TemplateResponse(Application::APP_ID, 'main');
$response->setContentSecurityPolicy($csp); $response->setContentSecurityPolicy($csp);
return $response; 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", "linkify-html": "^4.1.3",
"pinia": "^2.2.2", "pinia": "^2.2.2",
"toastify-js": "^1.12.0", "toastify-js": "^1.12.0",
"vite": "^5.4.1", "vite": "^5.4.2",
"vue": "^3.4.38", "vue": "^3.4.38",
"vue-material-design-icons": "^5.3.0", "vue-material-design-icons": "^5.3.0",
"vue-router": "^4.4.3" "vue-router": "^4.4.3"
@ -37,7 +37,8 @@
"@nextcloud/prettier-config": "^1.1.0", "@nextcloud/prettier-config": "^1.1.0",
"@nextcloud/stylelint-config": "^3.0.1", "@nextcloud/stylelint-config": "^3.0.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-pinia": "^0.4.0", "eslint-plugin-pinia": "^0.4.1",
"eslint-plugin-prettier": "^5.2.1" "eslint-plugin-prettier": "^5.2.1",
"vite-plugin-vue-devtools": "^7.3.9"
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<NcAppContent :class="{ padding: episode }"> <NcAppContent :class="{ episode }">
<slot /> <slot />
</NcAppContent> </NcAppContent>
</template> </template>
@ -21,7 +21,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
.padding { .episode {
padding-bottom: 6rem; padding-bottom: 6rem;
} }
</style> </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> <template>
<div class="flex"> <div class="flex">
<NcAvatar :display-name="name" :is-no-user="true" :size="256" :url="image" /> <NcAvatar
<h2>{{ name }}</h2> :display-name="episode.name"
<SafeHtml :source="description" /> :is-no-user="true"
:size="256"
:url="episode.image" />
<h2>{{ episode.name }}</h2>
<SafeHtml :source="episode.description" />
<div class="flex"> <div class="flex">
<NcButton v-if="link" :href="link" target="_blank"> <NcButton v-if="episode.link" :href="episode.link" target="_blank">
<template #icon> <template #icon>
<OpenInNewIcon :size="20" /> <OpenInNewIcon :size="20" />
</template> </template>
{{ title }} {{ episode.title }}
</NcButton> </NcButton>
<NcButton <NcButton
v-if="url" v-if="episode.url"
:download="filenameFromUrl(url)" :download="filenameFromUrl(episode.url)"
:href="url" :href="episode.url"
target="_blank"> target="_blank">
<template #icon> <template #icon>
<DownloadIcon :size="20" /> <DownloadIcon :size="20" />
</template> </template>
{{ t('repod', 'Download') }} {{ t('repod', 'Download') }}
{{ size ? `(${humanFileSize(size)})` : '' }} {{ episode.size ? `(${humanFileSize(episode.size)})` : '' }}
</NcButton> </NcButton>
</div> </div>
</div> </div>
@ -43,32 +47,8 @@ export default {
SafeHtml, SafeHtml,
}, },
props: { props: {
description: { episode: {
type: String, type: Object,
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,
required: true, required: true,
}, },
}, },

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@
<SafeHtml :source="description" /> <SafeHtml :source="description" />
</div> </div>
<NcAppNavigationNew <NcAppNavigationNew
v-if="!subscriptions.includes(url)" v-if="!getSubscriptions.includes(url)"
:text="t('repod', 'Subscribe')" :text="t('repod', 'Subscribe')"
@click="addSubscription"> @click="addSubscription">
<template #icon> <template #icon>
@ -79,7 +79,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(useSubscriptions, ['subscriptions']), ...mapState(useSubscriptions, ['getSubscriptions']),
url() { url() {
return decodeUrl(this.$route.params.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> <div>
<Loading v-if="loading" /> <Loading v-if="loading" />
<ul v-if="!loading"> <ul v-if="!loading">
<NcListItem <Episode
v-for="episode in filteredEpisodes" v-for="episode in filteredEpisodes"
:key="episode.guid" :key="episode.guid"
:active="isCurrentEpisode(episode)" :episode="episode"
:details="formatLocaleDate(new Date(episode.pubDate?.date))" :url="url" />
: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>
</ul> </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> </div>
</template> </template>
<script> <script>
import { import { hasEnded, isListening } from '../../utils/status.js'
NcActionButton, import Episode from './Episode.vue'
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 Loading from '../Atoms/Loading.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 axios from '@nextcloud/axios'
import { decodeUrl } from '../../utils/url.js'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { mapState } from 'pinia'
import { showError } from '../../utils/toast.js' import { showError } from '../../utils/toast.js'
import { usePlayer } from '../../store/player.js' import { usePlayer } from '../../store/player.js'
import { useSettings } from '../../store/settings.js' import { useSettings } from '../../store/settings.js'
@ -147,30 +26,13 @@ import { useSettings } from '../../store/settings.js'
export default { export default {
name: 'Episodes', name: 'Episodes',
components: { components: {
DownloadIcon, Episode,
Loading, Loading,
Modal,
NcActionButton,
NcActionLink,
NcActions,
NcAvatar,
NcListItem,
NcModal,
NcProgressBar,
OpenInNewIcon,
PlayIcon,
PlaylistPlayIcon,
PlaylistRemoveIcon,
StopIcon,
}, },
data() { data: () => ({
return {
episodes: [], episodes: [],
loading: true, loading: true,
loadingAction: false, }),
modalEpisode: null,
}
},
computed: { computed: {
...mapState(usePlayer, ['episode']), ...mapState(usePlayer, ['episode']),
...mapState(useSettings, ['filters']), ...mapState(useSettings, ['filters']),
@ -223,63 +85,8 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(usePlayer, ['load']), hasEnded,
filenameFromUrl, isListening,
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
}
},
}, },
} }
</script> </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) { @media only screen and (max-width: 768px) {
.infos { .infos {
flex: 2; flex: 1;
} }
.timer, .timer,

View File

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

View File

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

View File

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

View File

@ -44,12 +44,10 @@ export default {
NcAppNavigationItem, NcAppNavigationItem,
NcModal, NcModal,
}, },
data() { data: () => ({
return {
loading: false, loading: false,
modal: false, modal: false,
} }),
},
methods: { methods: {
generateUrl, generateUrl,
async importOpml(event) { async importOpml(event) {

View File

@ -2,8 +2,19 @@
<NcAppNavigationItem <NcAppNavigationItem
:loading="loading" :loading="loading"
:name="feed ? feed.title : url" :name="feed ? feed.title : url"
:to="hash"> :to="toFeedUrl(url)">
<template #actions> <template #actions>
<NcActionButton
:aria-label="t('repod', 'Favorite')"
:model-value="isFavorite"
:name="t('repod', 'Favorite')"
:title="t('repod', 'Favorite')"
@update:modelValue="switchFavorite($event)">
<template #icon>
<StarPlusIcon v-if="!isFavorite" :size="20" />
<StarRemoveIcon v-if="isFavorite" :size="20" />
</template>
</NcActionButton>
<NcActionButton <NcActionButton
:aria-label="t(`core`, 'Delete')" :aria-label="t(`core`, 'Delete')"
:name="t(`core`, 'Delete')" :name="t(`core`, 'Delete')"
@ -20,6 +31,7 @@
:display-name="feed.author || feed.title" :display-name="feed.author || feed.title"
:is-no-user="true" :is-no-user="true"
:url="feed.imageUrl" /> :url="feed.imageUrl" />
<StarIcon v-if="feed && isFavorite" class="star" :size="20" />
<AlertIcon v-if="failed" /> <AlertIcon v-if="failed" />
</template> </template>
</NcAppNavigationItem> </NcAppNavigationItem>
@ -27,23 +39,29 @@
<script> <script>
import { NcActionButton, NcAppNavigationItem, NcAvatar } from '@nextcloud/vue' import { NcActionButton, NcAppNavigationItem, NcAvatar } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia'
import AlertIcon from 'vue-material-design-icons/Alert.vue' import AlertIcon from 'vue-material-design-icons/Alert.vue'
import DeleteIcon from 'vue-material-design-icons/Delete.vue' import DeleteIcon from 'vue-material-design-icons/Delete.vue'
import StarIcon from 'vue-material-design-icons/Star.vue'
import StarPlusIcon from 'vue-material-design-icons/StarPlus.vue'
import StarRemoveIcon from 'vue-material-design-icons/StarRemove.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { mapActions } from 'pinia'
import { showError } from '../../utils/toast.js' import { showError } from '../../utils/toast.js'
import { toUrl } from '../../utils/url.js' import { toFeedUrl } from '../../utils/url.js'
import { useSubscriptions } from '../../store/subscriptions.js' import { useSubscriptions } from '../../store/subscriptions.js'
export default { export default {
name: 'Item', name: 'Subscription',
components: { components: {
AlertIcon, AlertIcon,
DeleteIcon, DeleteIcon,
NcActionButton, NcActionButton,
NcAppNavigationItem, NcAppNavigationItem,
NcAvatar, NcAvatar,
StarIcon,
StarPlusIcon,
StarRemoveIcon,
}, },
props: { props: {
url: { url: {
@ -51,16 +69,15 @@ export default {
required: true, required: true,
}, },
}, },
data() { data: () => ({
return {
failed: false, failed: false,
loading: true, loading: true,
feed: null, feed: null,
} }),
},
computed: { computed: {
hash() { ...mapState(useSubscriptions, ['getFavorites']),
return toUrl(this.url) isFavorite() {
return this.getFavorites.map((fav) => fav.url).includes(this.url)
}, },
}, },
async mounted() { async mounted() {
@ -74,6 +91,7 @@ export default {
), ),
) )
this.feed = podcastData.data.data this.feed = podcastData.data.data
this.editFavoriteData(this.url, podcastData.data.data)
} catch (e) { } catch (e) {
this.failed = true this.failed = true
console.error(e) console.error(e)
@ -82,7 +100,13 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(useSubscriptions, ['fetch']), ...mapActions(useSubscriptions, [
'fetch',
'addFavorite',
'editFavoriteData',
'removeFavorite',
]),
toFeedUrl,
async deleteSubscription() { async deleteSubscription() {
if ( if (
confirm( confirm(
@ -99,11 +123,33 @@ export default {
console.error(e) console.error(e)
showError(t('repod', 'Error while removing the feed')) showError(t('repod', 'Error while removing the feed'))
} finally { } finally {
this.removeFavorite(this.url)
this.loading = false this.loading = false
this.fetch() this.fetch()
} }
} }
}, },
switchFavorite(value) {
if (value) {
if (this.getFavorites.length >= 10) {
showError(t('repod', 'You can only have 10 favorites'))
return
}
this.addFavorite(this.url)
} else {
this.removeFavorite(this.url)
}
},
}, },
} }
</script> </script>
<style scoped>
.star {
bottom: 2px;
color: yellow;
left: 22px;
position: absolute;
}
</style>

View File

@ -2,7 +2,7 @@
<AppNavigation> <AppNavigation>
<template #list> <template #list>
<NcAppContentList> <NcAppContentList>
<router-link to="/"> <router-link to="/discover">
<NcAppNavigationNew :text="t('repod', 'Add a podcast')"> <NcAppNavigationNew :text="t('repod', 'Add a podcast')">
<template #icon> <template #icon>
<PlusIcon :size="20" /> <PlusIcon :size="20" />
@ -11,10 +11,17 @@
</router-link> </router-link>
<Loading v-if="loading" /> <Loading v-if="loading" />
<NcAppNavigationList v-if="!loading"> <NcAppNavigationList v-if="!loading">
<Item <Subscription
v-for="subscriptionUrl of subscriptions" v-for="url of getFavorites.map((fav) => fav.url)"
:key="subscriptionUrl" :key="url"
:url="subscriptionUrl" /> :url="url" />
<Subscription
v-for="url of getSubscriptions.filter(
(sub) =>
!getFavorites.map((fav) => fav.url).includes(sub),
)"
:key="url"
:url="url" />
</NcAppNavigationList> </NcAppNavigationList>
</NcAppContentList> </NcAppContentList>
</template> </template>
@ -32,10 +39,10 @@ import {
} from '@nextcloud/vue' } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import AppNavigation from '../Atoms/AppNavigation.vue' import AppNavigation from '../Atoms/AppNavigation.vue'
import Item from './Item.vue'
import Loading from '../Atoms/Loading.vue' import Loading from '../Atoms/Loading.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue' import PlusIcon from 'vue-material-design-icons/Plus.vue'
import Settings from '../Settings/Settings.vue' import Settings from '../Settings/Settings.vue'
import Subscription from './Subscription.vue'
import { showError } from '../../utils/toast.js' import { showError } from '../../utils/toast.js'
import { useSubscriptions } from '../../store/subscriptions.js' import { useSubscriptions } from '../../store/subscriptions.js'
@ -43,21 +50,19 @@ export default {
name: 'Subscriptions', name: 'Subscriptions',
components: { components: {
AppNavigation, AppNavigation,
Item,
Loading, Loading,
NcAppContentList, NcAppContentList,
NcAppNavigationList, NcAppNavigationList,
NcAppNavigationNew, NcAppNavigationNew,
PlusIcon, PlusIcon,
Settings, Settings,
Subscription,
}, },
data() { data: () => ({
return {
loading: true, loading: true,
} }),
},
computed: { computed: {
...mapState(useSubscriptions, ['subscriptions']), ...mapState(useSubscriptions, ['getSubscriptions', 'getFavorites']),
}, },
async mounted() { async mounted() {
try { try {

View File

@ -1,17 +1,22 @@
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import Discover from './views/Discover.vue' import Discover from './views/Discover.vue'
import Feed from './views/Feed.vue' import Feed from './views/Feed.vue'
import Home from './views/Home.vue'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(generateUrl('apps/repod')), history: createWebHistory(generateUrl('apps/repod')),
routes: [ routes: [
{ {
path: '/', path: '/',
component: Home,
},
{
path: '/discover',
component: Discover, component: Discover,
}, },
{ {
path: '/:url', path: '/feed/:url',
component: Feed, component: Feed,
}, },
], ],

View File

@ -44,12 +44,11 @@ export const usePlayer = defineStore('player', {
}), }),
) )
this.episode.action = action this.episode.action = action.data
} catch {} } catch {}
if ( if (
this.episode.action && this.episode.action &&
this.episode.action.position &&
this.episode.action.position < this.episode.action.total this.episode.action.position < this.episode.action.total
) { ) {
audio.currentTime = this.episode.action.position audio.currentTime = this.episode.action.position

View File

@ -1,14 +1,27 @@
import { getCookie, setCookie } from '../utils/cookies.js'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { setCookie } from '../utils/cookies.js'
export const useSettings = defineStore('settings', { export const useSettings = defineStore('settings', {
state: () => ({ state: () => {
try {
const filters = JSON.parse(getCookie('repod.filters'))
return {
filters: {
listened: filters.listened,
listening: filters.listening,
unlistened: filters.unlistened,
},
}
} catch {
return {
filters: { filters: {
listened: true, listened: true,
listening: true, listening: true,
unlistened: true, unlistened: true,
}, },
}), }
}
},
actions: { actions: {
setFilters(filters) { setFilters(filters) {
this.filters = { ...this.filters, ...filters } this.filters = { ...this.filters, ...filters }

View File

@ -1,11 +1,23 @@
import { getCookie, setCookie } from '../utils/cookies.js'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
export const useSubscriptions = defineStore('subscriptions', { export const useSubscriptions = defineStore('subscriptions', {
state: () => ({ state: () => ({
subscriptions: [], subs: [],
favs: [],
}), }),
getters: {
getSubscriptions: (state) => {
return state.subs
},
getFavorites: (state) => {
return state.favs
.filter((fav) => state.subs.includes(fav.url))
.sort((fav) => fav.lastPub)
},
},
actions: { actions: {
async fetch() { async fetch() {
const metrics = await axios.get( const metrics = await axios.get(
@ -14,7 +26,33 @@ export const useSubscriptions = defineStore('subscriptions', {
const subs = [...metrics.data.subscriptions].sort( const subs = [...metrics.data.subscriptions].sort(
(a, b) => b.listenedSeconds - a.listenedSeconds, (a, b) => b.listenedSeconds - a.listenedSeconds,
) )
this.subscriptions = subs.map((sub) => sub.url) this.subs = subs.map((sub) => sub.url)
try {
const favs = JSON.parse(getCookie('repod.favorites')) || []
this.favs = favs.map((url) => ({ url }))
} catch {}
},
addFavorite(url) {
this.favs.push({ url })
setCookie(
'repod.favorites',
JSON.stringify(this.favs.map((fav) => fav.url)),
365,
)
},
editFavoriteData(url, data) {
this.favs = this.favs.map((fav) =>
fav.url === url ? { ...fav, ...data } : fav,
)
},
removeFavorite(url) {
this.favs = this.favs.filter((fav) => fav.url !== url)
setCookie(
'repod.favorites',
JSON.stringify(this.favs.map((fav) => fav.url)),
365,
)
}, },
}, },
}) })

14
src/utils/status.js Normal file
View File

@ -0,0 +1,14 @@
export const hasEnded = (episode) =>
episode.action &&
episode.action.action &&
(episode.action.action.toLowerCase() === 'delete' ||
(episode.action.position > 0 &&
episode.action.total > 0 &&
episode.action.position >= episode.action.total))
export const isListening = (episode) =>
episode.action &&
episode.action.action &&
episode.action.action.toLowerCase() === 'play' &&
episode.action.position > 0 &&
!hasEnded(episode)

View File

@ -1,6 +1,6 @@
export const encodeUrl = (url) => encodeURIComponent(btoa(url)) export const encodeUrl = (url) => encodeURIComponent(btoa(url))
export const decodeUrl = (url) => atob(decodeURIComponent(url)) export const decodeUrl = (url) => atob(decodeURIComponent(url))
export const toUrl = (url) => `/${encodeUrl(url)}` export const toFeedUrl = (url) => `/feed/${encodeUrl(url)}`
export const filenameFromUrl = (str) => { export const filenameFromUrl = (str) => {
const url = new URL(str) const url = new URL(str)
return url.pathname.split('/').pop() return url.pathname.split('/').pop()

View File

@ -1,5 +1,5 @@
<template> <template>
<AppContent class="main"> <AppContent class="padding">
<NcTextField v-model="search" :label="t('repod', 'Find a podcast')"> <NcTextField v-model="search" :label="t('repod', 'Find a podcast')">
<template #icon> <template #icon>
<Magnify :size="20" /> <Magnify :size="20" />
@ -30,16 +30,14 @@ export default {
Search, Search,
Toplist, Toplist,
}, },
data() { data: () => ({
return {
search: '', search: '',
} }),
},
} }
</script> </script>
<style scoped> <style scoped>
.main { .padding {
padding: 15px 51px; padding: 15px 51px;
} }
</style> </style>

View File

@ -1,14 +1,14 @@
<template> <template>
<AppContent> <AppContent>
<Loading v-if="loading" /> <Loading v-if="loading" />
<NcEmptyContent <EmptyContent
v-if="failed" v-if="failed"
class="error" class="error"
:name="t('repod', 'Error loading feed')"> :name="t('repod', 'Error loading feed')">
<template #icon> <template #icon>
<Alert /> <Alert />
</template> </template>
</NcEmptyContent> </EmptyContent>
<Banner <Banner
v-if="feed" v-if="feed"
:author="feed.author" :author="feed.author"
@ -24,9 +24,9 @@
import Alert from 'vue-material-design-icons/Alert.vue' import Alert from 'vue-material-design-icons/Alert.vue'
import AppContent from '../components/Atoms/AppContent.vue' import AppContent from '../components/Atoms/AppContent.vue'
import Banner from '../components/Feed/Banner.vue' import Banner from '../components/Feed/Banner.vue'
import EmptyContent from '../components/Atoms/EmptyContent.vue'
import Episodes from '../components/Feed/Episodes.vue' import Episodes from '../components/Feed/Episodes.vue'
import Loading from '../components/Atoms/Loading.vue' import Loading from '../components/Atoms/Loading.vue'
import { NcEmptyContent } from '@nextcloud/vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { decodeUrl } from '../utils/url.js' import { decodeUrl } from '../utils/url.js'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
@ -37,17 +37,15 @@ export default {
Alert, Alert,
AppContent, AppContent,
Banner, Banner,
EmptyContent,
Episodes, Episodes,
Loading, Loading,
NcEmptyContent,
}, },
data() { data: () => ({
return {
failed: false, failed: false,
loading: true, loading: true,
feed: null, feed: null,
} }),
},
computed: { computed: {
url() { url() {
return decodeUrl(this.$route.params.url) return decodeUrl(this.$route.params.url)

View File

@ -1,6 +1,6 @@
<template> <template>
<NcAppContent class="content"> <AppContent>
<NcEmptyContent :name="t('repod', 'Missing required app')"> <EmptyContent class="empty" :name="t('repod', 'Missing required app')">
<template #action> <template #action>
<NcButton :href="gPodderSyncUrl"> <NcButton :href="gPodderSyncUrl">
{{ t('repod', 'Install GPodder Sync') }} {{ t('repod', 'Install GPodder Sync') }}
@ -9,22 +9,24 @@
<template #icon> <template #icon>
<Alert /> <Alert />
</template> </template>
</NcEmptyContent> </EmptyContent>
</NcAppContent> </AppContent>
</template> </template>
<script> <script>
import { NcAppContent, NcButton, NcEmptyContent } from '@nextcloud/vue'
import Alert from 'vue-material-design-icons/Alert.vue' import Alert from 'vue-material-design-icons/Alert.vue'
import AppContent from '../components/Atoms/AppContent.vue'
import EmptyContent from '../components/Atoms/EmptyContent.vue'
import { NcButton } from '@nextcloud/vue'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
export default { export default {
name: 'GPodder', name: 'GPodder',
components: { components: {
Alert, Alert,
NcAppContent, AppContent,
EmptyContent,
NcButton, NcButton,
NcEmptyContent,
}, },
computed: { computed: {
gPodderSyncUrl() { gPodderSyncUrl() {

42
src/views/Home.vue Normal file
View File

@ -0,0 +1,42 @@
<template>
<AppContent>
<EmptyContent
v-if="!getFavorites.length"
class="empty"
:description="
t('repod', 'Pin some subscriptions to see their latest updates')
"
:name="t('repod', 'No favorites')">
<template #icon>
<StarOffIcon :size="20" />
</template>
</EmptyContent>
<ul v-if="getFavorites.length">
<li v-for="url in getFavorites.map((fav) => fav.url)" :key="url">
<Favorites :url="url" />
</li>
</ul>
</AppContent>
</template>
<script>
import AppContent from '../components/Atoms/AppContent.vue'
import EmptyContent from '../components/Atoms/EmptyContent.vue'
import Favorites from '../components/Feed/Favorites.vue'
import StarOffIcon from 'vue-material-design-icons/StarOff.vue'
import { mapState } from 'pinia'
import { useSubscriptions } from '../store/subscriptions.js'
export default {
name: 'Home',
components: {
AppContent,
EmptyContent,
Favorites,
StarOffIcon,
},
computed: {
...mapState(useSubscriptions, ['getFavorites']),
},
}
</script>

View File

@ -83,21 +83,18 @@ msgstr "Abspielen"
msgid "Stop" msgid "Stop"
msgstr "Stopp" msgstr "Stopp"
msgid "Mark as read" msgid "Read"
msgstr "Als gelesen markieren" msgstr "Gelesen"
msgid "Mark as unread"
msgstr "Als ungelesen markieren"
msgid "Open website" msgid "Open website"
msgstr "Webseite aufrufen" msgstr "Webseite aufrufen"
msgid "Could not fetch episodes"
msgstr "Folgen können nicht abgerufen werden"
msgid "Could not change the status of the episode" msgid "Could not change the status of the episode"
msgstr "Kann den Status der Folge nicht ändern" msgstr "Kann den Status der Folge nicht ändern"
msgid "Could not fetch episodes"
msgstr "Folgen können nicht abgerufen werden"
msgid "Export subscriptions" msgid "Export subscriptions"
msgstr "Abonnements exportieren" msgstr "Abonnements exportieren"
@ -128,12 +125,18 @@ msgstr "Bewerte RePod ❤️"
msgid "Playback speed" msgid "Playback speed"
msgstr "Wiedergabegeschwindigkeit" msgstr "Wiedergabegeschwindigkeit"
msgid "Favorite"
msgstr "Favorit"
msgid "Are you sure you want to delete this subscription?" msgid "Are you sure you want to delete this subscription?"
msgstr "Bist Du sicher, dass Du das Abonnement löschen möchtest?" msgstr "Bist Du sicher, dass Du das Abonnement löschen möchtest?"
msgid "Error while removing the feed" msgid "Error while removing the feed"
msgstr "Fehler beim Löschen des Feeds" msgstr "Fehler beim Löschen des Feeds"
msgid "You can only have 10 favorites"
msgstr "Du kannst nur 10 Favoriten haben"
msgid "Add a podcast" msgid "Add a podcast"
msgstr "Einen Podcast hinzufügen" msgstr "Einen Podcast hinzufügen"
@ -151,3 +154,9 @@ msgstr "Benötigte App fehlt"
msgid "Install GPodder Sync" msgid "Install GPodder Sync"
msgstr "Installiere GPodder Sync" msgstr "Installiere GPodder Sync"
msgid "Pin some subscriptions to see their latest updates"
msgstr "Pinne einige Abonnements, um ihre neuesten Updates zu sehen"
msgid "No favorites"
msgstr "Keine Favoriten"

View File

@ -83,21 +83,18 @@ msgstr "Lecture"
msgid "Stop" msgid "Stop"
msgstr "Arrêter" msgstr "Arrêter"
msgid "Mark as read" msgid "Read"
msgstr "Marquer comme lu" msgstr "Lu"
msgid "Mark as unread"
msgstr "Marquer comme non lu"
msgid "Open website" msgid "Open website"
msgstr "Ouvrir le site web" msgstr "Ouvrir le site web"
msgid "Could not fetch episodes"
msgstr "Impossible de récuprer les épisodes"
msgid "Could not change the status of the episode" msgid "Could not change the status of the episode"
msgstr "Impossible de changer le status de l'épisode" msgstr "Impossible de changer le status de l'épisode"
msgid "Could not fetch episodes"
msgstr "Impossible de récuprer les épisodes"
msgid "Export subscriptions" msgid "Export subscriptions"
msgstr "Exporter les abonnements" msgstr "Exporter les abonnements"
@ -128,12 +125,18 @@ msgstr "Donnez votre avis ❤️"
msgid "Playback speed" msgid "Playback speed"
msgstr "Vitesse de lecture" msgstr "Vitesse de lecture"
msgid "Favorite"
msgstr "Favori"
msgid "Are you sure you want to delete this subscription?" msgid "Are you sure you want to delete this subscription?"
msgstr "Êtes-vous sûr de vouloir supprimer ce flux ?" msgstr "Êtes-vous sûr de vouloir supprimer ce flux ?"
msgid "Error while removing the feed" msgid "Error while removing the feed"
msgstr "Erreur lors de la suppression du flux" msgstr "Erreur lors de la suppression du flux"
msgid "You can only have 10 favorites"
msgstr "Vous ne pouvez avoir que 10 favoris"
msgid "Add a podcast" msgid "Add a podcast"
msgstr "Ajouter un podcast" msgstr "Ajouter un podcast"
@ -151,3 +154,9 @@ msgstr "Une application requise est manquante"
msgid "Install GPodder Sync" msgid "Install GPodder Sync"
msgstr "Installer GPodder Sync" msgstr "Installer GPodder Sync"
msgid "Pin some subscriptions to see their latest updates"
msgstr "Ajoutez des abonnements en favoris pour obtenir les dernières nouvelles ici"
msgid "No favorites"
msgstr "Aucun favoris"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Nextcloud 3.14159\n" "Project-Id-Version: Nextcloud 3.14159\n"
"Report-Msgid-Bugs-To: translations\\@example.com\n" "Report-Msgid-Bugs-To: translations\\@example.com\n"
"POT-Creation-Date: 2024-08-09 10:10+0000\n" "POT-Creation-Date: 2024-09-02 09:08+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -48,8 +48,8 @@ msgid ""
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:1 #: /app/specialVueFakeDummyForL10nScript.js:1
#: /app/specialVueFakeDummyForL10nScript.js:27 #: /app/specialVueFakeDummyForL10nScript.js:24
#: /app/specialVueFakeDummyForL10nScript.js:28 #: /app/specialVueFakeDummyForL10nScript.js:25
msgid "Download" msgid "Download"
msgstr "" msgstr ""
@ -106,96 +106,109 @@ msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:19 #: /app/specialVueFakeDummyForL10nScript.js:19
#: /app/specialVueFakeDummyForL10nScript.js:20 #: /app/specialVueFakeDummyForL10nScript.js:20
#: /app/specialVueFakeDummyForL10nScript.js:21 #: /app/specialVueFakeDummyForL10nScript.js:21
msgid "Mark as read" msgid "Read"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:22 #: /app/specialVueFakeDummyForL10nScript.js:22
#: /app/specialVueFakeDummyForL10nScript.js:23 #: /app/specialVueFakeDummyForL10nScript.js:23
#: /app/specialVueFakeDummyForL10nScript.js:24
msgid "Mark as unread"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:25
#: /app/specialVueFakeDummyForL10nScript.js:26
msgid "Open website" msgid "Open website"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:29 #: /app/specialVueFakeDummyForL10nScript.js:26
msgid "Could not fetch episodes"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:30
msgid "Could not change the status of the episode" msgid "Could not change the status of the episode"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:31 #: /app/specialVueFakeDummyForL10nScript.js:27
#: /app/specialVueFakeDummyForL10nScript.js:28
msgid "Could not fetch episodes"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:29
msgid "Export subscriptions" msgid "Export subscriptions"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:32 #: /app/specialVueFakeDummyForL10nScript.js:30
msgid "Filtering episodes" msgid "Filtering episodes"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:33 #: /app/specialVueFakeDummyForL10nScript.js:31
msgid "Show all" msgid "Show all"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:34 #: /app/specialVueFakeDummyForL10nScript.js:32
msgid "Listened" msgid "Listened"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:35 #: /app/specialVueFakeDummyForL10nScript.js:33
msgid "Listening" msgid "Listening"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:36 #: /app/specialVueFakeDummyForL10nScript.js:34
msgid "Unlistened" msgid "Unlistened"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:37 #: /app/specialVueFakeDummyForL10nScript.js:35
msgid "Import subscriptions" msgid "Import subscriptions"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:38 #: /app/specialVueFakeDummyForL10nScript.js:36
msgid "Import OPML file" msgid "Import OPML file"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:39 #: /app/specialVueFakeDummyForL10nScript.js:37
msgid "Rate RePod ❤️" msgid "Rate RePod ❤️"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:40 #: /app/specialVueFakeDummyForL10nScript.js:38
msgid "Playback speed" msgid "Playback speed"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:39
#: /app/specialVueFakeDummyForL10nScript.js:40
#: /app/specialVueFakeDummyForL10nScript.js:41 #: /app/specialVueFakeDummyForL10nScript.js:41
msgid "Are you sure you want to delete this subscription?" msgid "Favorite"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:42 #: /app/specialVueFakeDummyForL10nScript.js:42
msgid "Error while removing the feed" msgid "Are you sure you want to delete this subscription?"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:43 #: /app/specialVueFakeDummyForL10nScript.js:43
msgid "Add a podcast" msgid "Error while removing the feed"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:44 #: /app/specialVueFakeDummyForL10nScript.js:44
msgid "Could not fetch subscriptions" msgid "You can only have 10 favorites"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:45 #: /app/specialVueFakeDummyForL10nScript.js:45
msgid "Find a podcast" msgid "Add a podcast"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:46 #: /app/specialVueFakeDummyForL10nScript.js:46
msgid "Error loading feed" msgid "Could not fetch subscriptions"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:47 #: /app/specialVueFakeDummyForL10nScript.js:47
msgid "Missing required app" msgid "Find a podcast"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:48 #: /app/specialVueFakeDummyForL10nScript.js:48
msgid "Error loading feed"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:49
msgid "Missing required app"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:50
msgid "Install GPodder Sync" msgid "Install GPodder Sync"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:51
msgid "Pin some subscriptions to see their latest updates"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:52
msgid "No favorites"
msgstr ""

View File

@ -1,5 +1,6 @@
import { createAppConfig } from '@nextcloud/vite-config' import { createAppConfig } from '@nextcloud/vite-config'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vueDevTools from 'vite-plugin-vue-devtools'
const config = defineConfig(({ mode }) => ({ const config = defineConfig(({ mode }) => ({
build: { build: {
@ -10,8 +11,12 @@ const config = defineConfig(({ mode }) => ({
manualChunks: false, manualChunks: false,
}, },
}, },
sourcemap: mode === 'development', sourcemap: mode !== 'production',
}, },
define: {
__VUE_PROD_DEVTOOLS__: mode !== 'production'
},
plugins: [vueDevTools()],
})) }))
export default createAppConfig( export default createAppConfig(