Compare commits

...

33 Commits

Author SHA1 Message Date
5b08cf970e Merge pull request 'Migrate to vue3 (fix #126)' (#127) from vue3 into main
Some checks failed
repod / nodejs (push) Waiting to run
repod / release (push) Waiting to run
repod / xml (push) Successful in 14s
repod / php (push) Has been cancelled
Reviewed-on: #127
2024-08-17 12:24:26 +00:00
cf6dd25378 Merge branch 'main' into vue3
All checks were successful
repod / xml (push) Successful in 13s
repod / php (push) Successful in 1m1s
repod / nodejs (push) Successful in 59s
repod / release (push) Has been skipped
2024-08-17 14:18:57 +02:00
a2c3b389ba refactor: ♻️ move filename from url on utils
Some checks failed
repod / release (push) Waiting to run
repod / xml (push) Successful in 14s
repod / php (push) Successful in 1m5s
repod / nodejs (push) Has been cancelled
2024-08-17 14:17:24 +02:00
66b59c52fa revert: css is on js, no need for ignoring
All checks were successful
repod / xml (push) Successful in 13s
repod / php (push) Successful in 1m1s
repod / nodejs (push) Successful in 55s
repod / release (push) Has been skipped
2024-08-17 00:13:58 +02:00
36c3bd875b build: 📝 fix build and remove COC for now
All checks were successful
repod / xml (push) Successful in 15s
repod / php (push) Successful in 57s
repod / nodejs (push) Successful in 1m2s
repod / release (push) Has been skipped
2024-08-17 00:10:17 +02:00
5742d1a762 fix: fix episode ending status with watch
All checks were successful
repod / xml (push) Successful in 31s
repod / php (push) Successful in 1m13s
repod / nodejs (push) Successful in 1m18s
repod / release (push) Has been skipped
2024-08-16 23:50:36 +02:00
bd9e3691b9 chore: ⬆️ update pinia
All checks were successful
repod / xml (push) Successful in 24s
repod / php (push) Successful in 1m8s
repod / nodejs (push) Successful in 1m4s
repod / release (push) Has been skipped
2024-08-15 22:04:10 +02:00
19427809ca fix: add download on episode list
All checks were successful
repod / xml (push) Successful in 37s
repod / php (push) Successful in 1m12s
repod / nodejs (push) Successful in 1m57s
repod / release (push) Has been skipped
2024-08-15 21:50:44 +02:00
544c91edee fix: 🐛 fix loop action
All checks were successful
repod / xml (push) Successful in 24s
repod / php (push) Successful in 1m6s
repod / nodejs (push) Successful in 1m8s
repod / release (push) Has been skipped
2024-08-15 21:18:20 +02:00
cf6bd440bf refactor: move autoload to init in the right vue
All checks were successful
repod / xml (push) Successful in 18s
repod / php (push) Successful in 1m9s
repod / nodejs (push) Successful in 1m15s
repod / release (push) Has been skipped
2024-08-15 19:10:19 +02:00
891d4762d0 fix: ✏️ fix typo on setRate
All checks were successful
repod / xml (push) Successful in 23s
repod / php (push) Successful in 1m3s
repod / nodejs (push) Successful in 1m3s
repod / release (push) Has been skipped
2024-08-15 18:52:50 +02:00
a2b63241cc build: 🦺 add eslint plugin pinia and update deps 2024-08-15 18:52:34 +02:00
5db93914d2 fix: 🐛 fix filters
All checks were successful
repod / xml (push) Successful in 16s
repod / php (push) Successful in 1m11s
repod / nodejs (push) Successful in 1m10s
repod / release (push) Has been skipped
2024-08-15 17:07:15 +02:00
68ada2b0e0 fix: 🐛 fix current active episode
All checks were successful
repod / xml (push) Successful in 17s
repod / php (push) Successful in 1m7s
repod / nodejs (push) Successful in 1m15s
repod / release (push) Has been skipped
2024-08-15 16:42:51 +02:00
c91c17ae66 fix: 🐛 fix opacity
All checks were successful
repod / xml (push) Successful in 22s
repod / php (push) Successful in 1m4s
repod / nodejs (push) Successful in 1m9s
repod / release (push) Has been skipped
2024-08-15 16:25:11 +02:00
b4160d23d2 chore: ⬆️ update deps
All checks were successful
repod / xml (push) Successful in 1m36s
repod / php (push) Successful in 59s
repod / nodejs (push) Successful in 1m34s
repod / release (push) Has been skipped
2024-08-15 15:29:25 +02:00
caf0bb7ec0 fix: 🐛 fix forgotten player pinia trasition
All checks were successful
repod / xml (push) Successful in 13s
repod / php (push) Successful in 59s
repod / nodejs (push) Successful in 1m2s
repod / release (push) Has been skipped
2024-08-11 21:48:13 +02:00
5f528e6b9b revert: revert extra-actions
Some checks failed
repod / release (push) Waiting to run
repod / xml (push) Successful in 38s
repod / php (push) Successful in 1m10s
repod / nodejs (push) Has been cancelled
2024-08-11 21:45:10 +02:00
196bc23b1a feat: add download attribute to download button
All checks were successful
repod / xml (push) Successful in 12s
repod / php (push) Successful in 59s
repod / nodejs (push) Successful in 1m4s
repod / release (push) Has been skipped
2024-08-10 22:30:35 +02:00
e78e3b2565 fix: 🐛 fix modal alignment
All checks were successful
repod / xml (push) Successful in 13s
repod / php (push) Successful in 57s
repod / nodejs (push) Successful in 1m1s
repod / release (push) Has been skipped
2024-08-10 22:21:26 +02:00
b67123b578 fix: 🐛 fix modal
All checks were successful
repod / xml (push) Successful in 24s
repod / php (push) Successful in 1m4s
repod / nodejs (push) Successful in 1m6s
repod / release (push) Has been skipped
2024-08-10 22:05:06 +02:00
53543a259f fix: 🐛 fix description bug
All checks were successful
repod / xml (push) Successful in 30s
repod / php (push) Successful in 1m5s
repod / nodejs (push) Successful in 1m10s
repod / release (push) Has been skipped
2024-08-10 20:59:15 +02:00
7efb0327d4 fix: 🐛 fix prototype with async
All checks were successful
repod / xml (push) Successful in 1m47s
repod / php (push) Successful in 1m8s
repod / nodejs (push) Successful in 1m18s
repod / release (push) Has been skipped
2024-08-09 22:51:32 +02:00
81af0c219f fix: 🐛 fix crash NcTextField
Some checks failed
repod / xml (push) Successful in 1m38s
repod / php (push) Successful in 1m7s
repod / nodejs (push) Failing after 1m6s
repod / release (push) Has been skipped
2024-08-09 22:21:07 +02:00
1feb0291bb fix: 🎉 first working basic functionnalities
All checks were successful
repod / xml (push) Successful in 33s
repod / php (push) Successful in 1m8s
repod / nodejs (push) Successful in 1m23s
repod / release (push) Has been skipped
2024-08-09 16:50:24 +02:00
989d5d38e1 perf: improve build by removing sourcemaps
All checks were successful
repod / xml (push) Successful in 15s
repod / php (push) Successful in 1m0s
repod / nodejs (push) Successful in 1m9s
repod / release (push) Has been skipped
2024-08-09 10:10:58 +00:00
b4ccd98f77 fix: 📦 make it build !
Some checks failed
repod / xml (push) Successful in 13s
repod / php (push) Successful in 58s
repod / nodejs (push) Failing after 48s
repod / release (push) Has been skipped
2024-08-09 09:52:56 +00:00
75da02e05b refactor: 🏗️ rework to use pinia
Some checks failed
repod / xml (push) Successful in 1m38s
repod / php (push) Successful in 1m2s
repod / nodejs (push) Failing after 57s
repod / release (push) Has been skipped
2024-08-09 09:38:00 +00:00
1530e8b294 style: 🎨 remove useless spaces
Some checks failed
repod / xml (push) Successful in 16s
repod / php (push) Successful in 1m2s
repod / nodejs (push) Failing after 59s
repod / release (push) Has been skipped
2024-08-09 01:11:44 +02:00
7e359bdf29 fix: 📈 forgot time loop
Some checks failed
repod / xml (push) Successful in 12s
repod / php (push) Successful in 58s
repod / nodejs (push) Failing after 45s
repod / release (push) Has been skipped
2024-08-09 01:02:39 +02:00
082161e177 refactor: ♻️ rewrite player store to pinia
Some checks failed
repod / xml (push) Successful in 28s
repod / php (push) Successful in 1m3s
repod / nodejs (push) Failing after 58s
repod / release (push) Has been skipped
2024-08-09 00:58:25 +02:00
04ed6b101a build: 💄 use toastify instead of dialogs
Some checks failed
repod / xml (push) Successful in 33s
repod / php (push) Successful in 1m7s
repod / nodejs (push) Failing after 1m4s
repod / release (push) Has been skipped
2024-08-08 22:39:43 +02:00
6456ccc3d0 refactor: 🧱 first commit to enable vue3 support
Some checks failed
repod / xml (push) Successful in 27s
repod / php (push) Successful in 1m1s
repod / nodejs (push) Failing after 2m29s
repod / release (push) Has been skipped
2024-08-08 15:20:54 +00:00
38 changed files with 3877 additions and 5652 deletions

View File

@ -1,5 +1,9 @@
module.exports = {
extends: ['@nextcloud', 'plugin:prettier/recommended'],
extends: [
'@nextcloud',
'plugin:pinia/recommended',
'plugin:prettier/recommended',
],
rules: {
'jsdoc/require-jsdoc': 'off',
'vue/first-attribute-linebreak': 'off',

View File

@ -1,9 +0,0 @@
In the Nextcloud community, participants from all over the world come together to create Free Software for a free internet. This is made possible by the support, hard work and enthusiasm of thousands of people, including those who create and use Nextcloud software.
Our code of conduct offers some guidance to ensure Nextcloud participants can cooperate effectively in a positive and inspiring atmosphere, and to explain how together we can strengthen and support each other.
The Code of Conduct is shared by all contributors and users who engage with the Nextcloud team and its community services. It presents a summary of the shared values and “common sense” thinking in our community.
You can find our full code of conduct on our website: https://nextcloud.com/code-of-conduct/
Please, keep our CoC in mind when you contribute! That way, everyone can be a part of our community in a productive, positive, creative and fun way.

View File

@ -103,14 +103,14 @@ dist: build
source:
rm -rf $(source_build_directory)
mkdir -p $(source_build_directory)
tar cvzf $(source_package_name).tar.gz \
tar -C .. -cvzf $(source_package_name).tar.gz \
--exclude-vcs \
--exclude="../$(app_name)/build" \
--exclude="../$(app_name)/js/node_modules" \
--exclude="../$(app_name)/node_modules" \
--exclude="../$(app_name)/*.log" \
--exclude="../$(app_name)/js/*.log" \
../$(app_name)
--exclude="$(app_name)/build" \
--exclude="$(app_name)/js/node_modules" \
--exclude="$(app_name)/node_modules" \
--exclude="$(app_name)/*.log" \
--exclude="$(app_name)/js/*.log" \
$(app_name)
# Builds the source package for the app store, ignores php tests, js tests
# and build related folders that are unnecessary for an appstore release
@ -118,42 +118,43 @@ source:
appstore:
rm -rf $(appstore_build_directory)
mkdir -p $(appstore_build_directory)
tar cvzf $(appstore_package_name).tar.gz \
--exclude="../$(app_name)/build" \
--exclude="../$(app_name)/tests" \
--exclude="../$(app_name)/Makefile" \
--exclude="../$(app_name)/*.log" \
--exclude="../$(app_name)/phpunit*xml" \
--exclude="../$(app_name)/composer.*" \
--exclude="../$(app_name)/node_modules" \
--exclude="../$(app_name)/js/node_modules" \
--exclude="../$(app_name)/js/tests" \
--exclude="../$(app_name)/js/test" \
--exclude="../$(app_name)/js/*.log" \
--exclude="../$(app_name)/js/package.json" \
--exclude="../$(app_name)/js/bower.json" \
--exclude="../$(app_name)/js/karma.*" \
--exclude="../$(app_name)/js/protractor.*" \
--exclude="../$(app_name)/package.json" \
--exclude="../$(app_name)/bower.json" \
--exclude="../$(app_name)/karma.*" \
--exclude="../$(app_name)/protractor\.*" \
--exclude="../$(app_name)/.*" \
--exclude="../$(app_name)/js/.*" \
--exclude="../$(app_name)/webpack.js" \
--exclude="../$(app_name)/stylelint.config.js" \
--exclude="../$(app_name)/README.md" \
--exclude="../$(app_name)/package-lock.json" \
--exclude="../$(app_name)/LICENSE" \
--exclude="../$(app_name)/src" \
--exclude="../$(app_name)/stubs" \
--exclude="../$(app_name)/screens" \
--exclude="../$(app_name)/vendor" \
--exclude="../$(app_name)/translationfiles" \
--exclude="../$(app_name)/Dockerfile" \
--exclude="../$(app_name)/psalm.xml" \
--exclude="../$(app_name)/renovate.json" \
../$(app_name)
tar -C .. -cvzf $(appstore_package_name).tar.gz \
--exclude="$(app_name)/build" \
--exclude="$(app_name)/tests" \
--exclude="$(app_name)/Makefile" \
--exclude="$(app_name)/*.log" \
--exclude="$(app_name)/phpunit*xml" \
--exclude="$(app_name)/composer.*" \
--exclude="$(app_name)/node_modules" \
--exclude="$(app_name)/js/node_modules" \
--exclude="$(app_name)/js/tests" \
--exclude="$(app_name)/js/test" \
--exclude="$(app_name)/js/*.log" \
--exclude="$(app_name)/js/package.json" \
--exclude="$(app_name)/js/bower.json" \
--exclude="$(app_name)/js/karma.*" \
--exclude="$(app_name)/js/protractor.*" \
--exclude="$(app_name)/package.json" \
--exclude="$(app_name)/bower.json" \
--exclude="$(app_name)/karma.*" \
--exclude="$(app_name)/protractor\.*" \
--exclude="$(app_name)/.*" \
--exclude="$(app_name)/js/.*" \
--exclude="$(app_name)/webpack.js" \
--exclude="$(app_name)/stylelint.config.js" \
--exclude="$(app_name)/README.md" \
--exclude="$(app_name)/package-lock.json" \
--exclude="$(app_name)/LICENSE" \
--exclude="$(app_name)/src" \
--exclude="$(app_name)/stubs" \
--exclude="$(app_name)/screens" \
--exclude="$(app_name)/vendor" \
--exclude="$(app_name)/translationfiles" \
--exclude="$(app_name)/Dockerfile" \
--exclude="$(app_name)/psalm.xml" \
--exclude="$(app_name)/renovate.json" \
--exclude="$(app_name)/vite.config.mjs" \
$(app_name)
# Start a nextcloud server on Docker to kickstart developement
.PHONY: dev

18
composer.lock generated
View File

@ -1312,12 +1312,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "176422aa2c339a0f4e56b92862c67a94e2b584fb"
"reference": "251a4f1fefcc6e6cc90d50514fee6b6e3745cb3e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/176422aa2c339a0f4e56b92862c67a94e2b584fb",
"reference": "176422aa2c339a0f4e56b92862c67a94e2b584fb",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/251a4f1fefcc6e6cc90d50514fee6b6e3745cb3e",
"reference": "251a4f1fefcc6e6cc90d50514fee6b6e3745cb3e",
"shasum": ""
},
"conflict": {
@ -1414,7 +1414,7 @@
"codeigniter4/shield": "<1.0.0.0-beta8",
"codiad/codiad": "<=2.8.4",
"composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7",
"concrete5/concrete5": "<=9.3.2",
"concrete5/concrete5": "<9.3.3",
"concrete5/core": "<8.5.8|>=9,<9.1",
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
"contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4",
@ -1483,7 +1483,7 @@
"ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12",
"ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.35",
"ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8",
"ezsystems/ezplatform-richtext": ">=2.3,<2.3.7.1-dev",
"ezsystems/ezplatform-richtext": ">=2.3,<2.3.7.1-dev|>=3.3,<3.3.40",
"ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15",
"ezsystems/ezplatform-user": ">=1,<1.0.1",
"ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31",
@ -1564,6 +1564,7 @@
"hyn/multi-tenant": ">=5.6,<5.7.2",
"ibexa/admin-ui": ">=4.2,<4.2.3|>=4.6.0.0-beta1,<4.6.9",
"ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4|>=4.2,<4.2.3|>=4.5,<4.5.6|>=4.6,<4.6.2",
"ibexa/fieldtype-richtext": ">=4.6,<4.6.10",
"ibexa/graphql": ">=2.5,<2.5.31|>=3.3,<3.3.28|>=4.2,<4.2.3",
"ibexa/post-install": "<=1.0.4",
"ibexa/solr": ">=4.5,<4.5.4",
@ -1806,6 +1807,7 @@
"pubnub/pubnub": "<6.1",
"pusher/pusher-php-server": "<2.2.1",
"pwweb/laravel-core": "<=0.3.6.0-beta",
"pxlrbt/filament-excel": "<2.3.3",
"pyrocms/pyrocms": "<=3.9.1",
"qcubed/qcubed": "<=3.1.1",
"quickapps/cms": "<=2.0.0.0-beta2",
@ -1833,8 +1835,8 @@
"serluck/phpwhois": "<=4.2.6",
"sfroemken/url_redirect": "<=1.2.1",
"sheng/yiicms": "<=1.2",
"shopware/core": "<6.5.8.8-dev|>=6.6.0.0-RC1-dev,<6.6.1",
"shopware/platform": "<6.5.8.8-dev|>=6.6.0.0-RC1-dev,<6.6.1",
"shopware/core": "<=6.5.8.12|>=6.6,<=6.6.5",
"shopware/platform": "<=6.5.8.12|>=6.6,<=6.6.5",
"shopware/production": "<=6.3.5.2",
"shopware/shopware": "<=5.7.17",
"shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev",
@ -2119,7 +2121,7 @@
"type": "tidelift"
}
],
"time": "2024-08-05T22:04:39+00:00"
"time": "2024-08-14T19:05:08+00:00"
},
{
"name": "sebastian/diff",

8526
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,14 @@
"name": "repod",
"license": "AGPL-3.0-or-later",
"scripts": {
"build": "NODE_ENV=production webpack --config webpack.js --progress",
"dev": "NODE_ENV=development webpack --config webpack.js --progress",
"watch": "NODE_ENV=development webpack --config webpack.js --progress --watch",
"build": "vite build --mode production",
"dev": "vite build --mode development",
"dev:watch": "vite build --mode development --watch",
"watch": "npm run dev:watch",
"lint": "eslint src",
"stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css"
"lint:fix": "eslint src --fix",
"stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css",
"stylelint:fix": "stylelint src/**/*.vue src/**/*.scss src/**/*.css --fix"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
@ -14,27 +17,27 @@
"prettier": "@nextcloud/prettier-config",
"dependencies": {
"@nextcloud/axios": "^2.5.0",
"@nextcloud/dialogs": "^6.0.0",
"@nextcloud/initial-state": "^2.2.0",
"@nextcloud/l10n": "^3.1.0",
"@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^8.16.0",
"@nextcloud/vite-config": "^2.2.2",
"@nextcloud/vue": "9.0.0-alpha.5",
"dompurify": "^3.1.6",
"linkify-html": "^4.1.3",
"vue": "^2",
"pinia": "^2.2.2",
"toastify-js": "^1.12.0",
"vite": "^5.4.1",
"vue": "^3.4.38",
"vue-material-design-icons": "^5.3.0",
"vue-router": "^3",
"vuex": "^3"
"vue-router": "^4.4.3"
},
"devDependencies": {
"@nextcloud/browserslist-config": "^3.0.1",
"@nextcloud/eslint-config": "^8.4.1",
"@nextcloud/prettier-config": "^1.1.0",
"@nextcloud/stylelint-config": "^3.0.1",
"@nextcloud/webpack-vue-config": "^6.0.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-webpack-plugin": "^4.2.0",
"stylelint-webpack-plugin": "^5.0.1"
"eslint-plugin-pinia": "^0.2.0",
"eslint-plugin-prettier": "^5.2.1"
}
}

View File

@ -8,12 +8,14 @@
</template>
<script>
import '@nextcloud/dialogs/style.css'
import 'toastify-js/src/toastify.css'
import { mapActions, mapState } from 'pinia'
import Bar from './components/Player/Bar.vue'
import GPodder from './views/GPodder.vue'
import { NcContent } from '@nextcloud/vue'
import Subscriptions from './components/Sidebar/Subscriptions.vue'
import { loadState } from '@nextcloud/initial-state'
import { usePlayer } from './store/player.js'
export default {
name: 'App',
@ -24,9 +26,22 @@ export default {
Subscriptions,
},
computed: {
...mapState(usePlayer, ['paused']),
gpodder() {
return loadState('repod', 'gpodder', false)
},
},
mounted() {
this.init()
setInterval(this.loop, 40000)
},
methods: {
...mapActions(usePlayer, ['init', 'time']),
loop() {
if (this.paused === false) {
this.time()
}
},
},
}
</script>

View File

@ -1,11 +1,13 @@
<template>
<NcAppContent :class="episode ? 'padding' : ''">
<NcAppContent :class="{ padding: episode }">
<slot />
</NcAppContent>
</template>
<script>
import { NcAppContent } from '@nextcloud/vue'
import { mapState } from 'pinia'
import { usePlayer } from '../../store/player.js'
export default {
name: 'AppContent',
@ -13,9 +15,7 @@ export default {
NcAppContent,
},
computed: {
episode() {
return this.$store.state.player.episode
},
...mapState(usePlayer, ['episode']),
},
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<NcAppNavigation :class="episode ? 'padding' : ''">
<NcAppNavigation :class="{ padding: episode }">
<slot />
<template #list>
<slot name="list" />
@ -12,6 +12,8 @@
<script>
import { NcAppNavigation } from '@nextcloud/vue'
import { mapState } from 'pinia'
import { usePlayer } from '../../store/player.js'
export default {
name: 'AppNavigation',
@ -19,9 +21,7 @@ export default {
NcAppNavigation,
},
computed: {
episode() {
return this.$store.state.player.episode
},
...mapState(usePlayer, ['episode']),
},
}
</script>

View File

@ -1,20 +1,25 @@
<template>
<div>
<div class="flex">
<NcAvatar :display-name="name" :is-no-user="true" :size="256" :url="image" />
<h2>{{ name }}</h2>
<SafeHtml :source="description" />
<div>
<div class="flex">
<NcButton v-if="link" :href="link" target="_blank">
<template #icon>
<OpenInNewIcon :size="20" />
</template>
{{ title }}
</NcButton>
<NcButton v-if="url" :href="url" target="_blank">
<NcButton
v-if="url"
:download="filenameFromUrl(url)"
:href="url"
target="_blank">
<template #icon>
<DownloadIcon :size="20" />
</template>
{{ t('repod', 'Download') }} {{ size ? `(${episodeFileSize})` : '' }}
{{ t('repod', 'Download') }}
{{ size ? `(${humanFileSize(size)})` : '' }}
</NcButton>
</div>
</div>
@ -25,6 +30,7 @@ import { NcAvatar, NcButton } from '@nextcloud/vue'
import DownloadIcon from 'vue-material-design-icons/Download.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import SafeHtml from './SafeHtml.vue'
import { filenameFromUrl } from '../../utils/url.js'
import { humanFileSize } from '../../utils/size.js'
export default {
@ -66,16 +72,15 @@ export default {
required: true,
},
},
computed: {
episodeFileSize() {
return humanFileSize(this.size)
},
methods: {
filenameFromUrl,
humanFileSize,
},
}
</script>
<style scoped>
div {
.flex {
align-items: center;
display: flex;
flex-direction: column;

View File

@ -3,15 +3,15 @@
</template>
<script>
import dompurify from 'dompurify'
import linkifyHtml from 'linkify-html'
import { sanitize } from 'dompurify'
export default {
name: 'SafeHtml',
directives: {
sanitize: {
inserted(el, binding) {
el.innerHTML = sanitize(
mounted(el, binding) {
el.innerHTML = dompurify.sanitize(
linkifyHtml(binding.value, {
nl2br: true,
target: '_blank',

View File

@ -19,7 +19,7 @@
</template>
<template #actions>
<NcActionButton
v-if="!isSubscribed(feed.link)"
v-if="!subscriptions.includes(feed.link)"
:aria-label="t('repod', 'Subscribe')"
:name="t('repod', 'Subscribe')"
:title="t('repod', 'Subscribe')"
@ -36,14 +36,16 @@
<script>
import { NcActionButton, NcAvatar, NcListItem } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia'
import Loading from '../Atoms/Loading.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import axios from '@nextcloud/axios'
import { debounce } from '../../utils/debounce.js'
import { formatLocaleDate } from '../../utils/time.js'
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import { showError } from '../../utils/toast.js'
import { toUrl } from '../../utils/url.js'
import { useSubscriptions } from '../../store/subscriptions.js'
export default {
name: 'Search',
@ -66,12 +68,16 @@ export default {
loading: false,
}
},
computed: {
...mapState(useSubscriptions, ['subscriptions']),
},
watch: {
value() {
this.search()
},
},
methods: {
...mapActions(useSubscriptions, ['fetch']),
formatLocaleDate,
toUrl,
async addSubscription(url) {
@ -88,10 +94,7 @@ export default {
showError(t('repod', 'Error while adding the feed'))
}
this.$store.dispatch('subscriptions/fetch')
},
isSubscribed(url) {
return this.$store.state.subscriptions.subscriptions.includes(url)
this.fetch()
},
search: debounce(async function value() {
try {

View File

@ -16,7 +16,7 @@
import Loading from '../Atoms/Loading.vue'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import { showError } from '../../utils/toast.js'
import { toUrl } from '../../utils/url.js'
export default {

View File

@ -23,7 +23,7 @@
<SafeHtml :source="description" />
</div>
<NcAppNavigationNew
v-if="!isSubscribed"
v-if="!subscriptions.includes(url)"
:text="t('repod', 'Subscribe')"
@click="addSubscription">
<template #icon>
@ -37,13 +37,15 @@
<script>
import { NcAppNavigationNew, NcAvatar } from '@nextcloud/vue'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { mapActions, mapState } from 'pinia'
import { showError, showSuccess } from '../../utils/toast.js'
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import RssIcon from 'vue-material-design-icons/Rss.vue'
import SafeHtml from '../Atoms/SafeHtml.vue'
import axios from '@nextcloud/axios'
import { decodeUrl } from '../../utils/url.js'
import { generateUrl } from '@nextcloud/router'
import { useSubscriptions } from '../../store/subscriptions.js'
export default {
name: 'Banner',
@ -77,14 +79,13 @@ export default {
},
},
computed: {
...mapState(useSubscriptions, ['subscriptions']),
url() {
return decodeUrl(this.$route.params.url)
},
isSubscribed() {
return this.$store.state.subscriptions.subscriptions.includes(this.url)
},
},
methods: {
...mapActions(useSubscriptions, ['fetch']),
async addSubscription() {
try {
await axios.post(
@ -99,7 +100,7 @@ export default {
showError(t('repod', 'Error while adding the feed'))
}
this.$store.dispatch('subscriptions/fetch')
this.fetch()
},
copyFeed() {
window.navigator.clipboard.writeText(this.url)

View File

@ -6,18 +6,20 @@
v-for="episode in filteredEpisodes"
:key="episode.guid"
:active="isCurrentEpisode(episode)"
:class="hasEnded(episode) ? 'ended' : ''"
: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 #extra-actions>
<template #actions>
<NcActionButton
v-if="!isCurrentEpisode(episode)"
:aria-label="t('repod', 'Play')"
:title="t('repod', 'Play')"
@click="load(episode)">
@click="load(episode, url)">
<template #icon>
<PlayIcon :size="20" />
</template>
@ -32,7 +34,8 @@
</template>
</NcActionButton>
</template>
<template #actions>
<template #extra>
<NcActions>
<NcActionButton
v-if="episode.duration && !hasEnded(episode)"
:aria-label="t('repod', 'Mark as read')"
@ -67,6 +70,7 @@
</NcActionLink>
<NcActionLink
v-if="episode.url"
:download="filenameFromUrl(episode.url)"
:href="episode.url"
:name="t('repod', 'Download')"
target="_blank"
@ -75,6 +79,7 @@
<DownloadIcon :size="20" />
</template>
</NcActionLink>
</NcActions>
</template>
<template #icon>
<NcAvatar
@ -112,18 +117,20 @@
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 { EventBus } from '../../store/bus.js'
import Loading from '../Atoms/Loading.vue'
import Modal from '../Atoms/Modal.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
@ -132,9 +139,10 @@ 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 { showError } from '@nextcloud/dialogs'
import { showError } from '../../utils/toast.js'
import { usePlayer } from '../../store/player.js'
import { useSettings } from '../../store/settings.js'
export default {
name: 'Episodes',
@ -144,6 +152,7 @@ export default {
Modal,
NcActionButton,
NcActionLink,
NcActions,
NcAvatar,
NcListItem,
NcModal,
@ -163,12 +172,8 @@ export default {
}
},
computed: {
currentEpisode() {
return this.$store.state.player.episode
},
filters() {
return this.$store.state.settings.filters
},
...mapState(usePlayer, ['episode']),
...mapState(useSettings, ['filters']),
filteredEpisodes() {
return this.episodes.filter((episode) => {
if (!this.filters.listened && this.hasEnded(episode)) {
@ -190,6 +195,15 @@ export default {
return decodeUrl(this.$route.params.url)
},
},
watch: {
episode() {
if (this.episode) {
this.episodes = this.episodes.map((e) =>
e.url === this.episode.url ? this.episode : e,
)
}
},
},
async mounted() {
try {
this.loading = true
@ -201,7 +215,6 @@ export default {
this.episodes = [...episodes.data].sort(
(a, b) => new Date(b.pubDate?.date) - new Date(a.pubDate?.date),
)
EventBus.$on('updateEpisodesList', this.updateList)
} catch (e) {
console.error(e)
showError(t('repod', 'Could not fetch episodes'))
@ -209,10 +222,9 @@ export default {
this.loading = false
}
},
destroyed() {
EventBus.$off('updateEpisodesList')
},
methods: {
...mapActions(usePlayer, ['load']),
filenameFromUrl,
formatLocaleDate,
hasEnded(episode) {
return (
@ -224,18 +236,17 @@ export default {
)
},
isCurrentEpisode(episode) {
return this.currentEpisode && this.currentEpisode.url === episode.url
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)
)
},
load(episode) {
this.$store.dispatch('player/load', episode)
},
async markAs(episode, read) {
try {
this.loadingAction = true
@ -253,7 +264,9 @@ export default {
generateUrl('/apps/gpoddersync/episode_action/create'),
[episode.action],
)
this.updateList(episode)
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'))
@ -261,20 +274,11 @@ export default {
this.loadingAction = false
}
},
updateList(episode) {
this.episodes = this.episodes.map((e) =>
e.url === episode.url ? episode : e,
)
},
},
}
</script>
<style scoped>
.ended {
opacity: 0.4;
}
.progress {
margin-top: 0.4rem;
}

View File

@ -1,10 +1,10 @@
<template>
<div v-if="player.episode" class="footer">
<img class="background" :src="player.episode.image" />
<Loading v-if="!player.loaded" />
<ProgressBar v-if="player.loaded" />
<div v-if="player.loaded" class="player">
<img :src="player.episode.image" />
<div v-if="episode" class="footer">
<img class="background" :src="episode.image" />
<Loading v-if="!loaded" />
<ProgressBar v-if="loaded" />
<div v-if="loaded" class="player">
<img :src="episode.image" />
<Infos class="infos" />
<Controls class="controls" />
<Timer class="timer" />
@ -20,6 +20,8 @@ import Loading from '../Atoms/Loading.vue'
import ProgressBar from './ProgressBar.vue'
import Timer from './Timer.vue'
import Volume from './Volume.vue'
import { mapState } from 'pinia'
import { usePlayer } from '../../store/player.js'
export default {
name: 'Bar',
@ -32,9 +34,7 @@ export default {
Volume,
},
computed: {
player() {
return this.$store.state.player
},
...mapState(usePlayer, ['episode', 'loaded']),
},
}
</script>

View File

@ -1,21 +1,15 @@
<template>
<div class="controls">
<PauseIcon
v-if="!player.paused"
class="pointer"
:size="50"
@click="$store.dispatch('player/pause')" />
<PlayIcon
v-if="player.paused"
class="pointer"
:size="50"
@click="$store.dispatch('player/play')" />
<PauseIcon v-if="!paused" class="pointer" :size="50" @click="pause" />
<PlayIcon v-if="paused" class="pointer" :size="50" @click="play" />
</div>
</template>
<script>
import { mapActions, mapState } from 'pinia'
import PauseIcon from 'vue-material-design-icons/Pause.vue'
import PlayIcon from 'vue-material-design-icons/Play.vue'
import { usePlayer } from '../../store/player.js'
export default {
name: 'Controls',
@ -24,9 +18,10 @@ export default {
PlayIcon,
},
computed: {
player() {
return this.$store.state.player
...mapState(usePlayer, ['paused']),
},
methods: {
...mapActions(usePlayer, ['play', 'pause']),
},
}
</script>

View File

@ -1,20 +1,20 @@
<template>
<div class="root">
<strong class="pointer" @click="modal = true">
{{ player.episode.name }}
{{ episode.name }}
</strong>
<router-link :to="hash">
<i>{{ player.episode.title }}</i>
<i>{{ episode.title }}</i>
</router-link>
<NcModal v-if="modal" @close="modal = false">
<Modal
:description="player.episode.description"
:image="player.episode.image"
:link="player.episode.link"
:name="player.episode.name"
:size="player.episode.size"
:title="player.episode.title"
:url="player.episode.url" />
:description="episode.description"
:image="episode.image"
:link="episode.link"
:name="episode.name"
:size="episode.size"
:title="episode.title"
:url="episode.url" />
</NcModal>
</div>
</template>
@ -22,7 +22,9 @@
<script>
import Modal from '../Atoms/Modal.vue'
import { NcModal } from '@nextcloud/vue'
import { mapState } from 'pinia'
import { toUrl } from '../../utils/url.js'
import { usePlayer } from '../../store/player.js'
export default {
name: 'Infos',
@ -36,11 +38,9 @@ export default {
}
},
computed: {
player() {
return this.$store.state.player
},
...mapState(usePlayer, ['episode', 'podcastUrl']),
hash() {
return toUrl(this.player.podcastUrl)
return toUrl(this.podcastUrl)
},
},
}

View File

@ -1,20 +1,24 @@
<template>
<input
class="progress"
:max="player.duration"
:max="duration"
min="0"
type="range"
:value="player.currentTime"
@change="(event) => $store.dispatch('player/seek', event.target.value)" />
:value="currentTime"
@change="(event) => seek(event.target.value)" />
</template>
<script>
import { mapActions, mapState } from 'pinia'
import { usePlayer } from '../../store/player.js'
export default {
name: 'ProgressBar',
computed: {
player() {
return this.$store.state.player
...mapState(usePlayer, ['duration', 'currentTime']),
},
methods: {
...mapActions(usePlayer, ['seek']),
},
}
</script>

View File

@ -1,20 +1,20 @@
<template>
<div>
<span>{{ formatTimer(new Date(player.currentTime * 1000)) }}</span>
<span>{{ formatTimer(new Date(currentTime * 1000)) }}</span>
<span>/</span>
<span>{{ formatTimer(new Date(player.duration * 1000)) }}</span>
<span>{{ formatTimer(new Date(duration * 1000)) }}</span>
</div>
</template>
<script>
import { formatTimer } from '../../utils/time.js'
import { mapState } from 'pinia'
import { usePlayer } from '../../store/player.js'
export default {
name: 'Timer',
computed: {
player() {
return this.$store.state.player
},
...mapState(usePlayer, ['duration', 'currentTime']),
},
methods: {
formatTimer,

View File

@ -1,42 +1,42 @@
<template>
<div>
<VolumeHighIcon
v-if="player.volume > 0.7"
v-if="volume > 0.7"
class="pointer"
:size="30"
@click="mute" />
<VolumeLowIcon
v-if="player.volume > 0 && player.volume <= 0.3"
v-if="volume > 0 && volume <= 0.3"
class="pointer"
:size="30"
@click="mute" />
<VolumeMediumIcon
v-if="player.volume > 0.3 && player.volume <= 0.7"
v-if="volume > 0.3 && volume <= 0.7"
class="pointer"
:size="30"
@click="mute" />
<VolumeMuteIcon
v-if="player.volume === 0"
v-if="volume === 0"
class="pointer"
:size="30"
@click="$store.dispatch('player/volume', volumeMuted)" />
@click="setVolume(volumeMuted)" />
<input
max="1"
min="0"
step="0.1"
type="range"
:value="player.volume"
@change="
(event) => $store.dispatch('player/volume', event.target.value)
" />
:value="volume"
@change="(event) => setVolume(event.target.value)" />
</div>
</template>
<script>
import { mapActions, mapState } from 'pinia'
import VolumeHighIcon from 'vue-material-design-icons/VolumeHigh.vue'
import VolumeLowIcon from 'vue-material-design-icons/VolumeLow.vue'
import VolumeMediumIcon from 'vue-material-design-icons/VolumeMedium.vue'
import VolumeMuteIcon from 'vue-material-design-icons/VolumeMute.vue'
import { usePlayer } from '../../store/player.js'
export default {
name: 'Volume',
@ -52,14 +52,13 @@ export default {
}
},
computed: {
player() {
return this.$store.state.player
},
...mapState(usePlayer, ['volume']),
},
methods: {
...mapActions(usePlayer, ['setVolume']),
mute() {
this.volumeMuted = this.player.volume
this.$store.dispatch('player/volume', 0)
this.volumeMuted = this.volume
this.setVolume(0)
},
},
}

View File

@ -5,40 +5,30 @@
:name="t('repod', 'Filtering episodes')">
<template #actions>
<NcActionCheckbox
:checked="all"
:disabled="all"
@update:checked="
(checked) =>
$store.commit('settings/filters', {
listened: checked,
listening: checked,
unlistened: checked,
:model-value="all"
@change="
setFilters({
listened: true,
listening: true,
unlistened: true,
})
">
{{ t('repod', 'Show all') }}
</NcActionCheckbox>
<NcActionCheckbox
:checked="filters.listened"
@update:checked="
(checked) =>
$store.commit('settings/filters', { listened: checked })
">
:model-value="filters.listened"
@change="setFilters({ listened: !filters.listened })">
{{ t('repod', 'Listened') }}
</NcActionCheckbox>
<NcActionCheckbox
:checked="filters.listening"
@update:checked="
(checked) =>
$store.commit('settings/filters', { listening: checked })
">
:model-value="filters.listening"
@change="setFilters({ listening: !filters.listening })">
{{ t('repod', 'Listening') }}
</NcActionCheckbox>
<NcActionCheckbox
:checked="filters.unlistened"
@update:checked="
(checked) =>
$store.commit('settings/filters', { unlistened: checked })
">
:model-value="filters.unlistened"
@change="setFilters({ unlistened: !filters.unlistened })">
{{ t('repod', 'Unlistened') }}
</NcActionCheckbox>
</template>
@ -51,8 +41,11 @@
<script>
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 {
name: 'Filters',
@ -63,6 +56,7 @@ export default {
NcActionCheckbox,
},
computed: {
...mapState(useSettings, ['filters']),
all() {
return (
this.filters.listened &&
@ -70,12 +64,15 @@ export default {
this.filters.unlistened
)
},
filters() {
return this.$store.state.settings.filters
},
},
mounted() {
this.$store.dispatch('settings/fetch')
try {
const filters = getCookie('repod.filters')
this.filters = JSON.parse(filters)
} catch {}
},
methods: {
...mapActions(useSettings, ['setFilters']),
},
}
</script>

View File

@ -3,27 +3,27 @@
<template #extra>
<div class="extra">
<MinusIcon class="pointer" :size="20" @click="changeRate(-0.1)" />
<NcCounterBubble class="counter">
x{{ player.rate }}
</NcCounterBubble>
<NcCounterBubble class="counter">x{{ rate }}</NcCounterBubble>
<PlusIcon class="pointer" :size="20" @click="changeRate(0.1)" />
</div>
</template>
<template #icon>
<SpeedometerSlowIcon v-if="player.rate < 1" :size="20" />
<SpeedometerMediumIcon v-if="player.rate === 1" :size="20" />
<SpeedometerIcon v-if="player.rate > 1" :size="20" />
<SpeedometerSlowIcon v-if="rate < 1" :size="20" />
<SpeedometerMediumIcon v-if="rate === 1" :size="20" />
<SpeedometerIcon v-if="rate > 1" :size="20" />
</template>
</NcAppNavigationItem>
</template>
<script>
import { NcAppNavigationItem, NcCounterBubble } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia'
import MinusIcon from 'vue-material-design-icons/Minus.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import SpeedometerIcon from 'vue-material-design-icons/Speedometer.vue'
import SpeedometerMediumIcon from 'vue-material-design-icons/SpeedometerMedium.vue'
import SpeedometerSlowIcon from 'vue-material-design-icons/SpeedometerSlow.vue'
import { usePlayer } from '../../store/player.js'
export default {
name: 'Speed',
@ -37,17 +37,13 @@ export default {
SpeedometerSlowIcon,
},
computed: {
player() {
return this.$store.state.player
},
...mapState(usePlayer, ['rate']),
},
methods: {
...mapActions(usePlayer, ['setRate']),
changeRate(diff) {
const newRate = (this.player.rate + diff).toPrecision(2)
this.$store.dispatch(
'player/rate',
newRate > 0 ? newRate : this.player.rate,
)
const newRate = (this.rate + diff).toPrecision(2)
this.setRate(newRate > 0 ? newRate : this.rate)
},
},
}

View File

@ -31,8 +31,10 @@ import AlertIcon from 'vue-material-design-icons/Alert.vue'
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import { mapActions } from 'pinia'
import { showError } from '../../utils/toast.js'
import { toUrl } from '../../utils/url.js'
import { useSubscriptions } from '../../store/subscriptions.js'
export default {
name: 'Item',
@ -80,6 +82,7 @@ export default {
}
},
methods: {
...mapActions(useSubscriptions, ['fetch']),
async deleteSubscription() {
if (
confirm(
@ -97,7 +100,7 @@ export default {
showError(t('repod', 'Error while removing the feed'))
} finally {
this.loading = false
this.$store.dispatch('subscriptions/fetch')
this.fetch()
}
}
},

View File

@ -30,12 +30,14 @@ import {
NcAppNavigationList,
NcAppNavigationNew,
} from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia'
import AppNavigation from '../Atoms/AppNavigation.vue'
import Item from './Item.vue'
import Loading from '../Atoms/Loading.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import Settings from '../Settings/Settings.vue'
import { showError } from '@nextcloud/dialogs'
import { showError } from '../../utils/toast.js'
import { useSubscriptions } from '../../store/subscriptions.js'
export default {
name: 'Subscriptions',
@ -55,13 +57,11 @@ export default {
}
},
computed: {
subscriptions() {
return this.$store.state.subscriptions.subscriptions
},
...mapState(useSubscriptions, ['subscriptions']),
},
async mounted() {
try {
await this.$store.dispatch('subscriptions/fetch')
await this.fetch()
} catch (e) {
console.error(e)
showError(t('repod', 'Could not fetch subscriptions'))
@ -69,5 +69,8 @@ export default {
this.loading = false
}
},
methods: {
...mapActions(useSubscriptions, ['fetch']),
},
}
</script>

View File

@ -1,13 +1,13 @@
import { n, t } from '@nextcloud/l10n'
import App from './App.vue'
import Vue from 'vue'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router.js'
import store from './store/main.js'
const Vue = createApp(App)
const pinia = createPinia()
Vue.mixin({ methods: { t, n } })
const View = Vue.extend(App)
new View({
router,
store,
}).$mount('#content')
Vue.use(pinia)
Vue.use(router)
Vue.mount('#content')

View File

@ -1,13 +1,10 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import Discover from './views/Discover.vue'
import Feed from './views/Feed.vue'
import Router from 'vue-router'
import Vue from 'vue'
import { generateUrl } from '@nextcloud/router'
Vue.use(Router)
const router = new Router({
base: generateUrl('apps/repod'),
const router = createRouter({
history: createWebHashHistory(generateUrl('apps/repod')),
routes: [
{
path: '/',

View File

@ -1,3 +0,0 @@
import Vue from 'vue'
export const EventBus = new Vue()

View File

@ -1,17 +0,0 @@
import Vuex, { Store } from 'vuex'
import Vue from 'vue'
import { player } from './player.js'
import { settings } from './settings.js'
import { subscriptions } from './subscriptions.js'
Vue.use(Vuex)
const store = new Store({
modules: {
player,
settings,
subscriptions,
},
})
export default store

View File

@ -1,25 +1,12 @@
import { EventBus } from './bus.js'
import axios from '@nextcloud/axios'
import { decodeUrl } from '../utils/url.js'
import { defineStore } from 'pinia'
import { formatEpisodeTimestamp } from '../utils/time.js'
import { generateUrl } from '@nextcloud/router'
import router from '../router.js'
import store from './main.js'
const audio = new Audio()
audio.ondurationchange = () => store.commit('player/duration', audio.duration)
audio.onended = () => store.dispatch('player/stop')
audio.onloadeddata = () => store.commit('player/loaded', true)
audio.onplay = () => store.dispatch('player/play')
audio.onpause = () => store.dispatch('player/pause')
audio.onratechange = () => store.commit('player/rate', audio.playbackRate)
audio.onseeked = () => store.commit('player/currentTime', audio.currentTime)
audio.ontimeupdate = () => store.commit('player/currentTime', audio.currentTime)
audio.onvolumechange = () => store.commit('player/volume', audio.volume)
export const player = {
namespaced: true,
state: {
export const usePlayer = defineStore('player', {
state: () => ({
currentTime: null,
duration: null,
episode: null,
@ -29,119 +16,91 @@ export const player = {
volume: 1,
rate: 1,
started: 0,
},
mutations: {
action: (state, action) => {
state.episode.action = action
if (action && action.position && action.position < action.total) {
audio.currentTime = action.position
state.started = audio.currentTime
}
},
currentTime: (state, currentTime) => {
state.currentTime = currentTime
},
duration: (state, duration) => {
state.duration = duration
},
episode: (state, episode) => {
state.episode = episode
if (episode) {
state.podcastUrl = decodeUrl(router.currentRoute.params.url)
audio.src = episode.url
audio.load()
audio.play()
if (
episode.action &&
episode.action.position &&
episode.action.position < episode.action.total
) {
audio.currentTime = episode.action.position
state.started = audio.currentTime
}
} else {
state.loaded = false
state.podcastUrl = null
audio.src = ''
}
},
loaded: (state, loaded) => {
state.loaded = loaded
},
paused: (state, paused) => {
state.paused = paused
},
volume: (state, volume) => {
state.volume = volume
},
rate: (state, rate) => {
state.rate = rate
},
started: (state, started) => {
state.started = started
},
},
}),
actions: {
load: async (context, episode) => {
context.commit('episode', episode)
init() {
audio.ondurationchange = () => (this.duration = audio.duration)
audio.onended = () => this.stop()
audio.onloadeddata = () => (this.loaded = true)
audio.onpause = () => this.pause()
audio.onplay = () => this.play()
audio.onratechange = () => (this.rate = audio.playbackRate)
audio.onseeked = () => (this.currentTime = audio.currentTime)
audio.ontimeupdate = () => (this.currentTime = audio.currentTime)
audio.onvolumechange = () => (this.volume = audio.volume)
},
async load(episode, podcastUrl) {
this.episode = episode
this.podcastUrl = podcastUrl
if (this.episode) {
audio.src = this.episode.url
audio.load()
try {
const action = await axios.get(
generateUrl('/apps/repod/episodes/action?url={url}', {
url: episode.url,
url: this.episode.url,
}),
)
context.commit('action', action.data)
this.episode.action = action
} catch {}
},
pause: (context) => {
audio.pause()
context.commit('paused', true)
context.dispatch('time')
},
play: (context) => {
if (
this.episode.action &&
this.episode.action.position &&
this.episode.action.position < this.episode.action.total
) {
audio.currentTime = this.episode.action.position
this.started = audio.currentTime
}
audio.play()
context.commit('paused', false)
context.commit('started', audio.currentTime)
} else {
this.loaded = false
this.podcastUrl = null
audio.src = ''
}
},
seek: (context, currentTime) => {
pause() {
audio.pause()
this.paused = true
this.time()
},
play() {
audio.play()
this.paused = false
this.started = audio.currentTime
},
seek(currentTime) {
audio.currentTime = currentTime
context.dispatch('time')
this.time()
},
stop: (context) => {
context.dispatch('pause')
context.commit('episode', null)
stop() {
this.pause()
this.episode = null
},
time: async (context) => {
const episode = context.state.episode
episode.action = {
podcast: context.state.podcastUrl,
episode: context.state.episode.url,
guid: context.state.episode.guid,
time() {
this.episode.action = {
podcast: this.podcastUrl,
episode: this.episode.url,
guid: this.episode.guid,
action: 'play',
timestamp: formatEpisodeTimestamp(new Date()),
started: Math.round(context.state.started),
started: Math.round(this.started),
position: Math.round(audio.currentTime),
total: Math.round(audio.duration),
}
axios.post(generateUrl('/apps/gpoddersync/episode_action/create'), [
episode.action,
this.episode.action,
])
EventBus.$emit('updateEpisodesList', episode)
},
volume: (_, volume) => {
setVolume(volume) {
audio.volume = volume
},
rate: (_, rate) => {
setRate(rate) {
audio.playbackRate = rate
},
},
}
setInterval(() => {
if (player.state.paused === false) {
store.dispatch('player/time')
}
}, 40000)
})

View File

@ -1,28 +1,18 @@
import { getCookie, setCookie } from '../utils/cookies.js'
import { defineStore } from 'pinia'
import { setCookie } from '../utils/cookies.js'
export const settings = {
namespaced: true,
state: {
export const useSettings = defineStore('settings', {
state: () => ({
filters: {
listened: true,
listening: true,
unlistened: true,
},
},
mutations: {
filters: (state, filters) => {
state.filters = { ...state.filters, ...filters }
setCookie('repod.filters', JSON.stringify(state.filters), 365)
},
},
}),
actions: {
fetch: (context) => {
try {
const filters = getCookie('repod.filters')
context.commit('filters', JSON.parse(filters))
} catch (e) {
// nothing
}
setFilters(filters) {
this.filters = { ...this.filters, ...filters }
setCookie('repod.filters', JSON.stringify(this.filters), 365)
},
},
}
})

View File

@ -1,28 +1,20 @@
import axios from '@nextcloud/axios'
import { defineStore } from 'pinia'
import { generateUrl } from '@nextcloud/router'
export const subscriptions = {
namespaced: true,
state: {
export const useSubscriptions = defineStore('subscriptions', {
state: () => ({
subscriptions: [],
},
mutations: {
set: (state, subscriptions) => {
state.subscriptions = subscriptions
},
},
}),
actions: {
fetch: async (context) => {
async fetch() {
const metrics = await axios.get(
generateUrl('/apps/gpoddersync/personal_settings/metrics'),
)
const subs = [...metrics.data.subscriptions].sort(
(a, b) => b.listenedSeconds - a.listenedSeconds,
)
context.commit(
'set',
subs.map((sub) => sub.url),
)
this.subscriptions = subs.map((sub) => sub.url)
},
},
}
})

12
src/utils/toast.js Normal file
View File

@ -0,0 +1,12 @@
import toastify from 'toastify-js'
export const showMessage = (text, backgroundColor) =>
toastify({
text,
backgroundColor,
}).showToast()
export const showError = (text) => showMessage(text, 'var(--color-error)')
export const showWarning = (text) => showMessage(text, 'var(--color-warning)')
export const showInfo = (text) => showMessage(text, 'var(--color-primary)')
export const showSuccess = (text) => showMessage(text, 'var(--color-success)')

View File

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

View File

@ -1,7 +1,9 @@
<template>
<AppContent class="main">
<NcTextField :label="t('repod', 'Find a podcast')" :value.sync="search">
<NcTextField v-model="search" :label="t('repod', 'Find a podcast')">
<template #icon>
<Magnify :size="20" />
</template>
</NcTextField>
<Search v-if="search" :value="search" />
<Toplist v-if="!search" type="hot" />

View File

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

22
vite.config.mjs Normal file
View File

@ -0,0 +1,22 @@
import { createAppConfig } from '@nextcloud/vite-config'
import { defineConfig } from 'vite'
const config = defineConfig(({ mode }) => ({
build: {
rollupOptions: {
output: {
entryFileNames: 'js/[name].js',
format: 'iife',
manualChunks: false,
},
},
sourcemap: mode === 'development',
},
}))
export default createAppConfig(
{
main: 'src/main.js',
},
{ config },
)

View File

@ -1,31 +0,0 @@
const webpackConfig = require('@nextcloud/webpack-vue-config')
const ESLintPlugin = require('eslint-webpack-plugin')
const StyleLintPlugin = require('stylelint-webpack-plugin')
const path = require('path')
webpackConfig.entry = {
main: { import: path.join(__dirname, 'src', 'main.js'), filename: 'main.js' },
}
webpackConfig.plugins.push(
new ESLintPlugin({
extensions: ['js', 'vue'],
files: 'src',
}),
)
webpackConfig.plugins.push(
new StyleLintPlugin({
files: 'src/**/*.{css,scss,vue}',
}),
)
webpackConfig.module.rules.push({
test: /\.svg$/i,
type: 'asset/source',
})
webpackConfig.devtool =
webpackConfig.mode !== 'production' ? webpackConfig.devtool : false
module.exports = webpackConfig