Compare commits
210 Commits
Author | SHA1 | Date | |
---|---|---|---|
c3d6319402 | |||
|
bf40156f68 | ||
2dd9578257 | |||
7cd45d3539 | |||
8fd3eada75 | |||
|
3ebfbf6df7 | ||
133a8e8a09 | |||
|
d00e72439e | ||
4108ae57bc | |||
|
92ad21d49b | ||
761b3f1079 | |||
fb586e7115 | |||
|
11e0e0e675 | ||
|
f2de304d60 | ||
cf298db3d7 | |||
|
749fc6e628 | ||
bfebf4996f | |||
|
2a88b0d392 | ||
b8035a21ee | |||
a51fefb321 | |||
f1442c6c5f | |||
13515c3141 | |||
|
d53f9216c5 | ||
|
f4093378a9 | ||
|
6992ce6e50 | ||
|
f6630f91d8 | ||
70184c79b0 | |||
|
9e934e6ca6 | ||
0d017bbc68 | |||
|
3b2981d201 | ||
25949e98c9 | |||
b002e3bab8 | |||
d80044934f | |||
|
4d88f485fb | ||
|
ae66036a82 | ||
674f0193db | |||
ccc903be97 | |||
10c7a1f907 | |||
a6ab9c69d9 | |||
285e9f7dfb | |||
60fc620b79 | |||
5588bb93e3 | |||
b97d4487bb | |||
29c29cdfdd | |||
d7c3b87d8d | |||
9fa48c2da3 | |||
3cea8d3505 | |||
23280d68b9 | |||
1e1bb03c23 | |||
fc86f62d93 | |||
83e3358e9b | |||
38bc986bb3 | |||
eae106e72b | |||
3c358a3c5c | |||
|
79ee855f9b | ||
|
5150cb6501 | ||
1d85811ad3 | |||
872b0ced0a | |||
|
2a280c3493 | ||
|
c9f922b31d | ||
c983ab8d3b | |||
4f0685ccbd | |||
e165a070c8 | |||
|
622f5ec635 | ||
|
7d64e3370c | ||
fcf99e5bbf | |||
924106202a | |||
ce2412fb01 | |||
6c348d5583 | |||
|
7eef3ceaf0 | ||
|
5321c0a3bf | ||
fe40b7c9f7 | |||
|
11a1db72c6 | ||
7f00696140 | |||
|
33dcbe9162 | ||
5c6542e60c | |||
|
ea6704c537 | ||
df4ac80554 | |||
2b2ab2af8f | |||
|
7b00eb22ff | ||
|
ecee6ff2a8 | ||
57a22d9390 | |||
42c35d3856 | |||
4d199bc02b | |||
|
8a89cc06cd | ||
|
03740231c5 | ||
b54ab2be91 | |||
d1658a9408 | |||
1eb8b35501 | |||
fb7780fead | |||
19c56ef31d | |||
2bad1852d7 | |||
0302489924 | |||
81cb6a0191 | |||
094b7812cd | |||
491ad89242 | |||
01e2dabb65 | |||
a86ea6ab3f | |||
7b7ceef503 | |||
437c7868dd | |||
7c151d8f58 | |||
b7025a7aa1 | |||
42035d6e18 | |||
5ed33d1cf6 | |||
4e4730efd5 | |||
9005b519f3 | |||
b0132287f0 | |||
062da25264 | |||
fd0f8f9c7a | |||
|
35610ee8da | ||
60aedf3be5 | |||
4bafb3306b | |||
58af1f6d40 | |||
|
4eb9236ecb | ||
eb1196c841 | |||
dd275a1f03 | |||
f205d3243f | |||
46b30f1ebb | |||
9be107edc9 | |||
4e6eee96bf | |||
ddef81f92f | |||
456592adfc | |||
|
147311013c | ||
|
99dc3582c4 | ||
2824431330 | |||
a30678bfd2 | |||
6bc8d70016 | |||
c42d455d4b | |||
5b08cf970e | |||
cf6dd25378 | |||
a2c3b389ba | |||
5917c59610 | |||
|
6cf868dc51 | ||
66b59c52fa | |||
36c3bd875b | |||
5742d1a762 | |||
bd9e3691b9 | |||
19427809ca | |||
544c91edee | |||
cf6bd440bf | |||
891d4762d0 | |||
a2b63241cc | |||
5db93914d2 | |||
68ada2b0e0 | |||
c91c17ae66 | |||
b4160d23d2 | |||
caf0bb7ec0 | |||
5f528e6b9b | |||
196bc23b1a | |||
e78e3b2565 | |||
b67123b578 | |||
53543a259f | |||
7efb0327d4 | |||
81af0c219f | |||
1feb0291bb | |||
989d5d38e1 | |||
b4ccd98f77 | |||
75da02e05b | |||
1530e8b294 | |||
7e359bdf29 | |||
082161e177 | |||
04ed6b101a | |||
6456ccc3d0 | |||
e190a9eeb6 | |||
e63ff6ef04 | |||
a50947ffaf | |||
c97e927a15 | |||
a6a0f0d4f8 | |||
358c1d06c8 | |||
1e452263a7 | |||
|
bfa9cde661 | ||
0238107be2 | |||
f94b529d6d | |||
1302c82fdb | |||
|
08b327e408 | ||
2104e2e6d8 | |||
|
c73e53d04e | ||
cee28f51a6 | |||
|
1665e1e193 | ||
1d73de5925 | |||
|
a753d1d634 | ||
a91648848b | |||
a538b38160 | |||
8479cc1553 | |||
69a2c20338 | |||
55ee2634df | |||
f3090c5490 | |||
d57189490e | |||
d5c34fa719 | |||
|
57b581fa54 | ||
|
74fa105d5a | ||
d81e6b505c | |||
|
ef66427afc | ||
f9b0c8fa39 | |||
b8466aebf1 | |||
|
1aa6dad3ba | ||
|
0c0946559a | ||
fcf389f925 | |||
|
6c59851cb9 | ||
1d15ade42e | |||
|
c85a347032 | ||
ec605fde0a | |||
46b7d80652 | |||
541c669dd5 | |||
79c6fbcc16 | |||
42bfdb530b | |||
bc8db22fce | |||
|
a4a00d4aea | ||
8d13fac7aa | |||
|
83e519d4c5 |
@ -1,5 +1,11 @@
|
||||
module.exports = {
|
||||
extends: ['@nextcloud', 'plugin:prettier/recommended'],
|
||||
extends: [
|
||||
'@nextcloud',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
'plugin:pinia/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
rules: {
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'vue/first-attribute-linebreak': 'off',
|
@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
php:
|
||||
runs-on: ubuntu-latest
|
||||
container: nextcloud:29
|
||||
container: nextcloud:30
|
||||
steps:
|
||||
- run: apt-get update
|
||||
- run: apt-get install -y git nodejs
|
||||
@ -47,7 +47,7 @@ jobs:
|
||||
release:
|
||||
if: gitea.ref_type == 'tag'
|
||||
runs-on: ubuntu-latest
|
||||
container: nextcloud:29
|
||||
container: nextcloud:30
|
||||
steps:
|
||||
- run: apt-get update
|
||||
- run: apt-get install -y git nodejs
|
||||
|
1
.gitignore
vendored
@ -9,5 +9,6 @@
|
||||
|
||||
/node_modules/
|
||||
/js/
|
||||
/css/
|
||||
|
||||
/build/
|
||||
|
87
CHANGELOG.md
@ -1,15 +1,72 @@
|
||||
## 2.3.2 - 2024-05-31
|
||||
## 3.2.0 - Typing fast - 2024-09-15
|
||||
|
||||
### Added
|
||||
- 📝 Add Cardo to list of compatible clients
|
||||
[#176](https://github.com/thrillfall/nextcloud-gpodder/pull/176) reported by @n0vella
|
||||
|
||||
### Changed
|
||||
- 🧑💻 Switch entiere project to TypeScript
|
||||
|
||||
### Fixed
|
||||
- 💄 Missing icon on home when aren't any favorites
|
||||
- 💄 Tweaks spacing in several spaces on Home and banners
|
||||
- 💩 Leverage the available space between the episode title and the play button (but hacky way for now)
|
||||
[#59](https://git.crystalyx.net/Xefir/repod/issues/59#issuecomment-6246) reported by @W_LL_M
|
||||
|
||||
## 3.1.0 - Above the stars - 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 - What a vue - 2024-08-17
|
||||
|
||||
### Added
|
||||
- 🌐 Add german translation
|
||||
Thanks to @OiledAmoeba [#120](https://git.crystalyx.net/Xefir/repod/issues/120)
|
||||
|
||||
### Changed
|
||||
- 🎉 Migrate to Vue 3
|
||||
- 🔖 Support Nextcloud 30
|
||||
- 🏗️ Switch from Vuex to Pinia
|
||||
|
||||
### Fixed
|
||||
- 💄 Use iTunes image first for episode if available
|
||||
- 💄 Displaying styles and proper HTML on episode's modal descriptions
|
||||
|
||||
### Removed
|
||||
- 🗑️ Temporary replacing @nextcloud/dialogs to toastyjs
|
||||
|
||||
## 2.3.3 - The Cake is a Lie - 2024-06-14
|
||||
|
||||
### Changed
|
||||
- ⬆️ Update @nextcloud/dialogs to 5.3.2
|
||||
|
||||
### Fixed
|
||||
- 🐛 App crashed when no cache system available
|
||||
[#107](https://git.crystalyx.net/Xefir/repod/issues/107) reported by @skvaller and @PhilTraere
|
||||
|
||||
## 2.3.2 - Young Youth - 2024-05-31
|
||||
|
||||
### Fixed
|
||||
- 🐛 New subscribe button on search not disapearing if subscribed
|
||||
- ♿ Missing accessibility label on this button as well
|
||||
|
||||
## 2.3.1 - 2024-05-29
|
||||
## 2.3.1 - Powerwash the Universe - 2024-05-29
|
||||
|
||||
### Changed
|
||||
- ⚡ Reduce app size by not shipping sourcemap
|
||||
|
||||
## 2.3.0 - 2024-05-29
|
||||
## 2.3.0 - Star Align - 2024-05-29
|
||||
|
||||
### Added
|
||||
- ➕ Ability to subscribe to podcast from search list
|
||||
@ -26,12 +83,12 @@
|
||||
- ⚰️ Drop support for PHP 8.0
|
||||
- 🌐 Removed babel
|
||||
|
||||
## 2.2.1 - 2024-05-18
|
||||
## 2.2.1 - Shami was here - 2024-05-18
|
||||
|
||||
### Removed
|
||||
- ♻️ Rollback: Hide unreadable episodes because of insecure sources
|
||||
|
||||
## 2.2.0 - 2024-05-18
|
||||
## 2.2.0 - Moving in and out - 2024-05-18
|
||||
|
||||
### Added
|
||||
- 🚨 Linting the code with ESLint
|
||||
@ -43,7 +100,7 @@
|
||||
### Fixed
|
||||
- 🔓 Hide unreadable episodes because of insecure sources
|
||||
|
||||
## 2.1.0 - 2024-03-16
|
||||
## 2.1.0 - Pocket Gundams - 2024-03-16
|
||||
|
||||
### Added
|
||||
- 🔍 Add CTA for rating the app on the store
|
||||
@ -57,7 +114,7 @@
|
||||
- 🔒 App wasn't working for non admin users
|
||||
[#76](https://git.crystalyx.net/Xefir/repod/issues/76) reported by @devasservice
|
||||
|
||||
## 2.0.0 - 2024-03-05
|
||||
## 2.0.0 - Taking Actions - 2024-03-05
|
||||
|
||||
### Added
|
||||
- 🍪 Saving filters preference
|
||||
@ -74,13 +131,13 @@
|
||||
### Fixed
|
||||
- ❤️🔥 Better handling ended episodes
|
||||
|
||||
## 1.5.9 - 2024-02-21
|
||||
## 1.5.9 - Just According to Keikaku - 2024-02-21
|
||||
|
||||
### Changed
|
||||
- 🧑💻 Change some endpoints to match gPodder.net "specifications"
|
||||
- ⬆️ Update @nextcloud/vue to 8.7.0
|
||||
|
||||
## 1.5.8 - 2024-02-11
|
||||
## 1.5.8 - Goblet of Eonothem - 2024-02-11
|
||||
|
||||
### Fixed
|
||||
- Fyyd API sometime send empty feeds, ignoring them
|
||||
@ -98,19 +155,19 @@
|
||||
### Changed
|
||||
- Update @nextcloud/vue to v8.6.2
|
||||
|
||||
## 1.5.5 - 2024-02-04
|
||||
## 1.5.5 - Hide and seek - 2024-02-04
|
||||
|
||||
### Fixed
|
||||
- Can't open podcast details if cache missing or misconfigured
|
||||
[#58](https://git.crystalyx.net/Xefir/repod/issues/58) reported by @raxventus
|
||||
|
||||
## 1.5.4 - 2024-02-03
|
||||
## 1.5.4 - In search of the truth - 2024-02-03
|
||||
|
||||
### Fixed
|
||||
- Nextcloud search engine didn't work on Nextcloud 26 and 27
|
||||
[#57](https://git.crystalyx.net/Xefir/repod/issues/57) reported by @JonOfUs
|
||||
|
||||
## 1.5.3 - 2024-02-01
|
||||
## 1.5.3 - The date where it all ends - 2024-02-01
|
||||
|
||||
### Changed
|
||||
- Update @nextcloud/vue to v8.6.1
|
||||
@ -118,7 +175,7 @@
|
||||
### Fixed
|
||||
- Fix episode listing crashing if an invalid publication date is present in the RSS
|
||||
|
||||
## 1.5.2 - 2024-02-01
|
||||
## 1.5.2 - A little to the top - 2024-02-01
|
||||
|
||||
### Changed
|
||||
- Update @nextcloud/router to v3.0.0
|
||||
@ -126,7 +183,7 @@
|
||||
### Fixed
|
||||
- Fix player alignment off by a couple of pixels
|
||||
|
||||
## 1.5.1 - 2024-01-30
|
||||
## 1.5.1 - Play on the PlayHead - 2024-01-30
|
||||
|
||||
### Changed
|
||||
- Update @nextcloud/vue to v8.6.0
|
||||
@ -136,7 +193,7 @@
|
||||
### Fixed
|
||||
- Force the placement of the filter settings to the top
|
||||
|
||||
## 1.5.0 - 2024-01-30
|
||||
## 1.5.0 - Featuring the filtering - 2024-01-30
|
||||
|
||||
### Added
|
||||
- Filtering options for each podcast section
|
||||
|
@ -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.
|
12
Dockerfile
@ -1,10 +1,10 @@
|
||||
FROM nextcloud:29
|
||||
FROM nextcloud:30
|
||||
|
||||
ENV NEXTCLOUD_UPDATE 1
|
||||
ENV NEXTCLOUD_ADMIN_USER repod
|
||||
ENV NEXTCLOUD_ADMIN_PASSWORD repod
|
||||
ENV NEXTCLOUD_INIT_HTACCESS 1
|
||||
ENV SQLITE_DATABASE repod
|
||||
ENV NEXTCLOUD_UPDATE=1
|
||||
ENV NEXTCLOUD_ADMIN_USER=repod
|
||||
ENV NEXTCLOUD_ADMIN_PASSWORD=repod
|
||||
ENV NEXTCLOUD_INIT_HTACCESS=1
|
||||
ENV SQLITE_DATABASE=repod
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y nodejs npm sqlite3 && \
|
||||
|
87
Makefile
@ -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)/tsconfig.json" \
|
||||
--exclude="$(app_name)/stylelint.config.cjs" \
|
||||
--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.ts" \
|
||||
$(app_name)
|
||||
|
||||
# Start a nextcloud server on Docker to kickstart developement
|
||||
.PHONY: dev
|
||||
|
@ -12,7 +12,6 @@ You need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) inst
|
||||
- Import and export subscriptions
|
||||
- Mobile friendly interface
|
||||
- Unified search integration
|
||||
- Interface in multiple languages
|
||||
|
||||
## Comparaison with similar apps for Nextcloud
|
||||
|
||||
@ -25,17 +24,24 @@ You need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) inst
|
||||
| Listen synced episodes by another clients | ✅ | ✅ | ❌ | ❌ |
|
||||
| Fetch and listen new epidodes | ✅ | ❌ | ✅ | ✅ |
|
||||
| Keep track of listened episodes | ✅ | ✅ | ❌ | ✅ |
|
||||
| Download epidodes | ✅ | ✅ | ❌ | ✅ |
|
||||
| Import and export subscriptions | ✅ | ❌ | ❌ | ❌ |
|
||||
| Search and discover new podcasts | ✅ | ❌ | ❌ | ✅ |
|
||||
| Open episode website and RSS feed | ✅ | ✅ | ❌ | ✅ |
|
||||
| Integrate with Nextcloud search engine | ✅ | ❌ | ❌ | ✅ |
|
||||
| Integrate with [Nextcloud Notes](https://apps.nextcloud.com/apps/notes) | ❌ | ✅ | ❌ | ❌ |
|
||||
| Mobile friendly interface | ✅ | ❌ | ✅ | ✅ |
|
||||
| Support chapters | ❌ | ❌ | ❌ | ✅ |
|
||||
| Available in multiple languages | ⭕ (en/fr/de) | ❌ | ✅ | ⭕ (en/de) |
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Homepage
|
||||
![homepage](./screens/index.png)
|
||||
|
||||
### Discover
|
||||
![homepage](./screens/discover.png)
|
||||
|
||||
### Search
|
||||
![search](./screens/search.png)
|
||||
|
||||
@ -52,6 +58,7 @@ You need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) inst
|
||||
| [AntennaPod](https://antennapod.org) | Initial purpose for this project, as a synchronization endpoint for this client.<br> Support is available [as of version 2.5.1](https://github.com/AntennaPod/AntennaPod/pull/5243/). |
|
||||
| [KDE Kasts](https://apps.kde.org/de/kasts/) | Supported since version 21.12 |
|
||||
| [Podcast Merlin](https://github.com/yoyoooooooooo/Podcast-Merlin--Nextcloud-Gpodder-Client-For-Windows) | Full sync support podcast client for Windows |
|
||||
| [Cardo](https://n0vella.github.io/#/cardo) | Podcast client with sync support, for Windows, Mac and Linux |
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -7,11 +7,14 @@
|
||||
<description><![CDATA[## Features
|
||||
- 🔍 Browse and subscribe huge collection of podcasts
|
||||
- 🔊 Listen to episodes directly in Nextcloud
|
||||
- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/)
|
||||
- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/) and [other apps](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-gpoddersync)
|
||||
- 📱 Mobile friendly interface
|
||||
- 📡 Import and export your subscriptions
|
||||
- ➡️ Full features comparison [here](https://git.crystalyx.net/Xefir/repod#comparaison-with-similar-apps-for-nextcloud)
|
||||
|
||||
## Requirements
|
||||
You need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installed to use this app!]]></description>
|
||||
<version>2.3.2</version>
|
||||
<version>3.2.0</version>
|
||||
<licence>agpl</licence>
|
||||
<author mail="xefir@crystalyx.net" homepage="https://crystalyx.net">Michel Roux</author>
|
||||
<namespace>RePod</namespace>
|
||||
@ -20,12 +23,13 @@ You need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) inst
|
||||
<website>https://git.crystalyx.net/Xefir/repod</website>
|
||||
<bugs>https://git.crystalyx.net/Xefir/repod/issues</bugs>
|
||||
<screenshot>https://git.crystalyx.net/Xefir/repod/raw/branch/main/screens/index.png</screenshot>
|
||||
<screenshot>https://git.crystalyx.net/Xefir/repod/raw/branch/main/screens/discover.png</screenshot>
|
||||
<screenshot>https://git.crystalyx.net/Xefir/repod/raw/branch/main/screens/search.png</screenshot>
|
||||
<screenshot>https://git.crystalyx.net/Xefir/repod/raw/branch/main/screens/episodes.png</screenshot>
|
||||
<screenshot>https://git.crystalyx.net/Xefir/repod/raw/branch/main/screens/modal.png</screenshot>
|
||||
<dependencies>
|
||||
<php min-version="8.1"/>
|
||||
<nextcloud min-version="27" max-version="29"/>
|
||||
<nextcloud min-version="27" max-version="30"/>
|
||||
</dependencies>
|
||||
<navigations>
|
||||
<navigation>
|
||||
|
@ -13,6 +13,8 @@ declare(strict_types=1);
|
||||
return [
|
||||
'routes' => [
|
||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||
['name' => 'page#feed', 'url' => '/feed/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+']],
|
||||
['name' => 'page#discover', 'url' => '/discover', 'verb' => 'GET'],
|
||||
['name' => 'episodes#action', 'url' => '/episodes/action', 'verb' => 'GET'],
|
||||
['name' => 'episodes#list', 'url' => '/episodes/list', 'verb' => 'GET'],
|
||||
['name' => 'opml#export', 'url' => '/opml/export', 'verb' => 'GET'],
|
||||
|
@ -15,10 +15,10 @@
|
||||
"psalm": "psalm --threads=1 --no-cache --show-info=true"
|
||||
},
|
||||
"require-dev": {
|
||||
"nextcloud/ocp": "^29.0.1",
|
||||
"nextcloud/ocp": "^30.0.0",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"nextcloud/coding-standard": "^1.2.1",
|
||||
"vimeo/psalm": "^5.24.0"
|
||||
"nextcloud/coding-standard": "^1.3.2",
|
||||
"vimeo/psalm": "^5.26.1"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
|
552
composer.lock
generated
48
l10n/de.js
Normal file
@ -0,0 +1,48 @@
|
||||
OC.L10N.register(
|
||||
"repod",
|
||||
{
|
||||
"RePod Subscriptions" : "RePod Abonnements",
|
||||
"Podcast" : "Podcast",
|
||||
"RePod" : "RePod",
|
||||
"🔊 Browse, manage and listen to podcasts" : "🔊 Suchen, Verwalten und Anhören von Podcasts",
|
||||
"## Features\n- 🔍 Browse and subscribe huge collection of podcasts\n- 🔊 Listen to episodes directly in Nextcloud\n- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/) and [other apps](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-gpoddersync)\n- 📱 Mobile friendly interface\n- 📡 Import and export your subscriptions\n- ➡️ Full features comparison [here](https://git.crystalyx.net/Xefir/repod#comparaison-with-similar-apps-for-nextcloud)\n\n## Requirements\nYou need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installed to use this app!" : "## Funktionen\n- 🔍 Durchsuchen und abonnieren einer großen Sammlung von Podcasts\n- 🔊 Episoden direkt in Nextcloud anhören\n- 🌐 Synchronisiere deine Aktivität mit [AntennaPod](https://antennapod.org/)\n- 📱 Mobile-freundliche Schnittstelle\n- 📡 Importieren und Exportieren Ihrer Abonnements\n\n## Voraussetzungen\nDu musst [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installiert haben, um diese App zu benutzen!",
|
||||
"Download" : "Herunterladen",
|
||||
"Add a RSS link" : "Einen RSS-Link hinzufügen",
|
||||
"Subscribe" : "Abonnieren",
|
||||
"Error while adding the feed" : "Fehler beim Hinzufügen des Feeds",
|
||||
"Could not fetch search results" : "Suchergebnisse können nicht geladen werden",
|
||||
"New podcasts" : "Neue Podcasts",
|
||||
"Hot podcasts" : "Beliebte Podcasts",
|
||||
"Could not fetch tops" : "Titel können nicht abgerufen werden",
|
||||
"Copy feed" : "Feed kopieren",
|
||||
"Link copied to the clipboard" : "Der Link des Feeds wurde in die Zwischenablage kopiert",
|
||||
"Play" : "Abspielen",
|
||||
"Stop" : "Stopp",
|
||||
"Read" : "Gelesen",
|
||||
"Open website" : "Webseite aufrufen",
|
||||
"Could not change the status of the episode" : "Kann den Status der Folge nicht ändern",
|
||||
"Could not fetch episodes" : "Folgen können nicht abgerufen werden",
|
||||
"Export subscriptions" : "Abonnements exportieren",
|
||||
"Filtering episodes" : "Folgen filtern",
|
||||
"Show all" : "Zeige alles",
|
||||
"Listened" : "Gehört",
|
||||
"Listening" : "Läuft",
|
||||
"Unlistened" : "Nicht angehört",
|
||||
"Import subscriptions" : "Importiere Abonnements",
|
||||
"Import OPML file" : "Importiere OPML-Datei",
|
||||
"Rate RePod ❤️" : "Bewerte RePod ❤️",
|
||||
"Playback speed" : "Wiedergabegeschwindigkeit",
|
||||
"Favorite" : "Favorit",
|
||||
"Are you sure you want to delete this subscription?" : "Bist Du sicher, dass Du das Abonnement löschen möchtest?",
|
||||
"Error while removing the feed" : "Fehler beim Löschen des Feeds",
|
||||
"You can only have 10 favorites" : "Du kannst nur 10 Favoriten haben",
|
||||
"Add a podcast" : "Einen Podcast hinzufügen",
|
||||
"Could not fetch subscriptions" : "Abonnements können nicht abgerufen werden",
|
||||
"Find a podcast" : "Finde einen Podcast",
|
||||
"Error loading feed" : "Fehler beim Laden des Feeds",
|
||||
"Missing required app" : "Benötigte App fehlt",
|
||||
"Install GPodder Sync" : "Installiere GPodder Sync",
|
||||
"Pin some subscriptions to see their latest updates" : "Pinne einige Abonnements, um ihre neuesten Updates zu sehen",
|
||||
"No favorites" : "Keine Favoriten"
|
||||
},
|
||||
"");
|
46
l10n/de.json
Normal file
@ -0,0 +1,46 @@
|
||||
{ "translations": {
|
||||
"RePod Subscriptions" : "RePod Abonnements",
|
||||
"Podcast" : "Podcast",
|
||||
"RePod" : "RePod",
|
||||
"🔊 Browse, manage and listen to podcasts" : "🔊 Suchen, Verwalten und Anhören von Podcasts",
|
||||
"## Features\n- 🔍 Browse and subscribe huge collection of podcasts\n- 🔊 Listen to episodes directly in Nextcloud\n- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/) and [other apps](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-gpoddersync)\n- 📱 Mobile friendly interface\n- 📡 Import and export your subscriptions\n- ➡️ Full features comparison [here](https://git.crystalyx.net/Xefir/repod#comparaison-with-similar-apps-for-nextcloud)\n\n## Requirements\nYou need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installed to use this app!" : "## Funktionen\n- 🔍 Durchsuchen und abonnieren einer großen Sammlung von Podcasts\n- 🔊 Episoden direkt in Nextcloud anhören\n- 🌐 Synchronisiere deine Aktivität mit [AntennaPod](https://antennapod.org/)\n- 📱 Mobile-freundliche Schnittstelle\n- 📡 Importieren und Exportieren Ihrer Abonnements\n\n## Voraussetzungen\nDu musst [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installiert haben, um diese App zu benutzen!",
|
||||
"Download" : "Herunterladen",
|
||||
"Add a RSS link" : "Einen RSS-Link hinzufügen",
|
||||
"Subscribe" : "Abonnieren",
|
||||
"Error while adding the feed" : "Fehler beim Hinzufügen des Feeds",
|
||||
"Could not fetch search results" : "Suchergebnisse können nicht geladen werden",
|
||||
"New podcasts" : "Neue Podcasts",
|
||||
"Hot podcasts" : "Beliebte Podcasts",
|
||||
"Could not fetch tops" : "Titel können nicht abgerufen werden",
|
||||
"Copy feed" : "Feed kopieren",
|
||||
"Link copied to the clipboard" : "Der Link des Feeds wurde in die Zwischenablage kopiert",
|
||||
"Play" : "Abspielen",
|
||||
"Stop" : "Stopp",
|
||||
"Read" : "Gelesen",
|
||||
"Open website" : "Webseite aufrufen",
|
||||
"Could not change the status of the episode" : "Kann den Status der Folge nicht ändern",
|
||||
"Could not fetch episodes" : "Folgen können nicht abgerufen werden",
|
||||
"Export subscriptions" : "Abonnements exportieren",
|
||||
"Filtering episodes" : "Folgen filtern",
|
||||
"Show all" : "Zeige alles",
|
||||
"Listened" : "Gehört",
|
||||
"Listening" : "Läuft",
|
||||
"Unlistened" : "Nicht angehört",
|
||||
"Import subscriptions" : "Importiere Abonnements",
|
||||
"Import OPML file" : "Importiere OPML-Datei",
|
||||
"Rate RePod ❤️" : "Bewerte RePod ❤️",
|
||||
"Playback speed" : "Wiedergabegeschwindigkeit",
|
||||
"Favorite" : "Favorit",
|
||||
"Are you sure you want to delete this subscription?" : "Bist Du sicher, dass Du das Abonnement löschen möchtest?",
|
||||
"Error while removing the feed" : "Fehler beim Löschen des Feeds",
|
||||
"You can only have 10 favorites" : "Du kannst nur 10 Favoriten haben",
|
||||
"Add a podcast" : "Einen Podcast hinzufügen",
|
||||
"Could not fetch subscriptions" : "Abonnements können nicht abgerufen werden",
|
||||
"Find a podcast" : "Finde einen Podcast",
|
||||
"Error loading feed" : "Fehler beim Laden des Feeds",
|
||||
"Missing required app" : "Benötigte App fehlt",
|
||||
"Install GPodder Sync" : "Installiere GPodder Sync",
|
||||
"Pin some subscriptions to see their latest updates" : "Pinne einige Abonnements, um ihre neuesten Updates zu sehen",
|
||||
"No favorites" : "Keine Favoriten"
|
||||
},"pluralForm" :""
|
||||
}
|
13
l10n/fr.js
@ -5,7 +5,7 @@ OC.L10N.register(
|
||||
"Podcast" : "Podcast",
|
||||
"RePod" : "RePod",
|
||||
"🔊 Browse, manage and listen to podcasts" : "🔊 Parcourir, gérer et écouter vos podcasts",
|
||||
"## Features\n- 🔍 Browse and subscribe huge collection of podcasts\n- 🔊 Listen to episodes directly in Nextcloud\n- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/)\n\n## Requirements\nYou need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installed to use this app!" : "## Fonctionnalités\n- 🔍 Parcourir et s'abonner à une grande collections de podcasts\n- 🔊 Écouter vos épisodes directement sur Nextcloud\n- 🌐 Synchroniser son activité avec [AntennaPod](https://antennapod.org/)\n\n## Pré-requis\nVous devez avoir [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installé pour utiliser cette application !",
|
||||
"## Features\n- 🔍 Browse and subscribe huge collection of podcasts\n- 🔊 Listen to episodes directly in Nextcloud\n- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/) and [other apps](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-gpoddersync)\n- 📱 Mobile friendly interface\n- 📡 Import and export your subscriptions\n- ➡️ Full features comparison [here](https://git.crystalyx.net/Xefir/repod#comparaison-with-similar-apps-for-nextcloud)\n\n## Requirements\nYou need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installed to use this app!" : "## Fonctionnalités\n- 🔍 Parcourir et s'abonner à une grande collections de podcasts\n- 🔊 Écouter vos épisodes directement sur Nextcloud\n- 🌐 Synchroniser son activité avec [AntennaPod](https://antennapod.org/) et d'autres [applications](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-gpoddersync)\n- 📱 Interface optimisée pour mobiles et ordinateurs\n- 📡 Import/export de ses abonnements\n- ➡️ Tableau récapitulatif complet des fonctionnalitées [ici](https://git.crystalyx.net/Xefir/repod#comparaison-with-similar-apps-for-nextcloud)\n\n## Pré-requis\nVous devez avoir [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installé pour utiliser cette application !",
|
||||
"Download" : "Télécharger",
|
||||
"Add a RSS link" : "Ajouter un lien RSS",
|
||||
"Subscribe" : "S'abonner",
|
||||
@ -15,13 +15,13 @@ OC.L10N.register(
|
||||
"Hot podcasts" : "Tendances",
|
||||
"Could not fetch tops" : "Impossible de récupérer les tops",
|
||||
"Copy feed" : "Copier le flux",
|
||||
"Link copied to the clipboard" : "Lien vers le flux copié dans le presse-papiers",
|
||||
"Play" : "Lecture",
|
||||
"Stop" : "Arrêter",
|
||||
"Mark as read" : "Marquer comme lu",
|
||||
"Mark as unread" : "Marquer comme non lu",
|
||||
"Read" : "Lu",
|
||||
"Open website" : "Ouvrir le site web",
|
||||
"Could not fetch episodes" : "Impossible de récuprer les épisodes",
|
||||
"Could not change the status of the episode" : "Impossible de changer le status de l'épisode",
|
||||
"Could not fetch episodes" : "Impossible de récuprer les épisodes",
|
||||
"Export subscriptions" : "Exporter les abonnements",
|
||||
"Filtering episodes" : "Filtrage des épisodes",
|
||||
"Show all" : "Montrer tout",
|
||||
@ -32,14 +32,17 @@ OC.L10N.register(
|
||||
"Import OPML file" : "Importer un fichier OPML",
|
||||
"Rate RePod ❤️" : "Donnez votre avis ❤️",
|
||||
"Playback speed" : "Vitesse de lecture",
|
||||
"Favorite" : "Favori",
|
||||
"Are you sure you want to delete this subscription?" : "Êtes-vous sûr de vouloir supprimer ce flux ?",
|
||||
"Error while removing the feed" : "Erreur lors de la suppression du flux",
|
||||
"You can only have 10 favorites" : "Vous ne pouvez avoir que 10 favoris",
|
||||
"Add a podcast" : "Ajouter un podcast",
|
||||
"Could not fetch subscriptions" : "Impossible de récupérer les flux",
|
||||
"Find a podcast" : "Chercher un podcast",
|
||||
"Error loading feed" : "Erreur lors du chargement du flux",
|
||||
"Missing required app" : "Une application requise est manquante",
|
||||
"Install GPodder Sync" : "Installer GPodder Sync",
|
||||
"Feed's link copied to the clipboard" : "Lien vers le flux copié dans le presse-papiers"
|
||||
"Pin some subscriptions to see their latest updates" : "Ajoutez des abonnements en favoris pour obtenir les dernières nouvelles ici",
|
||||
"No favorites" : "Aucun favoris"
|
||||
},
|
||||
"");
|
||||
|
13
l10n/fr.json
@ -3,7 +3,7 @@
|
||||
"Podcast" : "Podcast",
|
||||
"RePod" : "RePod",
|
||||
"🔊 Browse, manage and listen to podcasts" : "🔊 Parcourir, gérer et écouter vos podcasts",
|
||||
"## Features\n- 🔍 Browse and subscribe huge collection of podcasts\n- 🔊 Listen to episodes directly in Nextcloud\n- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/)\n\n## Requirements\nYou need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installed to use this app!" : "## Fonctionnalités\n- 🔍 Parcourir et s'abonner à une grande collections de podcasts\n- 🔊 Écouter vos épisodes directement sur Nextcloud\n- 🌐 Synchroniser son activité avec [AntennaPod](https://antennapod.org/)\n\n## Pré-requis\nVous devez avoir [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installé pour utiliser cette application !",
|
||||
"## Features\n- 🔍 Browse and subscribe huge collection of podcasts\n- 🔊 Listen to episodes directly in Nextcloud\n- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/) and [other apps](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-gpoddersync)\n- 📱 Mobile friendly interface\n- 📡 Import and export your subscriptions\n- ➡️ Full features comparison [here](https://git.crystalyx.net/Xefir/repod#comparaison-with-similar-apps-for-nextcloud)\n\n## Requirements\nYou need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installed to use this app!" : "## Fonctionnalités\n- 🔍 Parcourir et s'abonner à une grande collections de podcasts\n- 🔊 Écouter vos épisodes directement sur Nextcloud\n- 🌐 Synchroniser son activité avec [AntennaPod](https://antennapod.org/) et d'autres [applications](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-gpoddersync)\n- 📱 Interface optimisée pour mobiles et ordinateurs\n- 📡 Import/export de ses abonnements\n- ➡️ Tableau récapitulatif complet des fonctionnalitées [ici](https://git.crystalyx.net/Xefir/repod#comparaison-with-similar-apps-for-nextcloud)\n\n## Pré-requis\nVous devez avoir [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) installé pour utiliser cette application !",
|
||||
"Download" : "Télécharger",
|
||||
"Add a RSS link" : "Ajouter un lien RSS",
|
||||
"Subscribe" : "S'abonner",
|
||||
@ -13,13 +13,13 @@
|
||||
"Hot podcasts" : "Tendances",
|
||||
"Could not fetch tops" : "Impossible de récupérer les tops",
|
||||
"Copy feed" : "Copier le flux",
|
||||
"Link copied to the clipboard" : "Lien vers le flux copié dans le presse-papiers",
|
||||
"Play" : "Lecture",
|
||||
"Stop" : "Arrêter",
|
||||
"Mark as read" : "Marquer comme lu",
|
||||
"Mark as unread" : "Marquer comme non lu",
|
||||
"Read" : "Lu",
|
||||
"Open website" : "Ouvrir le site web",
|
||||
"Could not fetch episodes" : "Impossible de récuprer les épisodes",
|
||||
"Could not change the status of the episode" : "Impossible de changer le status de l'épisode",
|
||||
"Could not fetch episodes" : "Impossible de récuprer les épisodes",
|
||||
"Export subscriptions" : "Exporter les abonnements",
|
||||
"Filtering episodes" : "Filtrage des épisodes",
|
||||
"Show all" : "Montrer tout",
|
||||
@ -30,14 +30,17 @@
|
||||
"Import OPML file" : "Importer un fichier OPML",
|
||||
"Rate RePod ❤️" : "Donnez votre avis ❤️",
|
||||
"Playback speed" : "Vitesse de lecture",
|
||||
"Favorite" : "Favori",
|
||||
"Are you sure you want to delete this subscription?" : "Êtes-vous sûr de vouloir supprimer ce flux ?",
|
||||
"Error while removing the feed" : "Erreur lors de la suppression du flux",
|
||||
"You can only have 10 favorites" : "Vous ne pouvez avoir que 10 favoris",
|
||||
"Add a podcast" : "Ajouter un podcast",
|
||||
"Could not fetch subscriptions" : "Impossible de récupérer les flux",
|
||||
"Find a podcast" : "Chercher un podcast",
|
||||
"Error loading feed" : "Erreur lors du chargement du flux",
|
||||
"Missing required app" : "Une application requise est manquante",
|
||||
"Install GPodder Sync" : "Installer GPodder Sync",
|
||||
"Feed's link copied to the clipboard" : "Lien vers le flux copié dans le presse-papiers"
|
||||
"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" :""
|
||||
}
|
@ -32,6 +32,10 @@ class Application extends App implements IBootstrap
|
||||
/** @var IInitialState $initialState */
|
||||
$initialState = $appContainer->get(IInitialState::class);
|
||||
|
||||
if (null === $appManager->getAppInfo(self::GPODDERSYNC_ID)) {
|
||||
$appManager->disableApp(self::GPODDERSYNC_ID);
|
||||
}
|
||||
|
||||
$gpoddersync = $appManager->isEnabledForUser(self::GPODDERSYNC_ID);
|
||||
if (!$gpoddersync) {
|
||||
try {
|
||||
|
@ -8,24 +8,57 @@ use OCA\RePod\AppInfo\Application;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IRequest;
|
||||
use OCP\Util;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
private IConfig $config
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function index(): TemplateResponse {
|
||||
Util::addScript(Application::APP_ID, 'main');
|
||||
Util::addScript(Application::APP_ID, Application::APP_ID.'-main');
|
||||
Util::addStyle(Application::APP_ID, Application::APP_ID.'-main');
|
||||
|
||||
$csp = new ContentSecurityPolicy();
|
||||
$csp->addAllowedImageDomain('*');
|
||||
$csp->addAllowedMediaDomain('*');
|
||||
|
||||
if ($this->config->getSystemValueBool('debug', false)) {
|
||||
/** @psalm-suppress DeprecatedMethod */
|
||||
$csp->allowEvalScript();
|
||||
$csp->addAllowedConnectDomain('*');
|
||||
$csp->addAllowedScriptDomain('*');
|
||||
}
|
||||
|
||||
$response = new TemplateResponse(Application::APP_ID, 'main');
|
||||
$response->setContentSecurityPolicy($csp);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function discover(): TemplateResponse {
|
||||
return $this->index();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function feed(): TemplateResponse {
|
||||
return $this->index();
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,14 @@ use OCA\RePod\AppInfo\Application;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IRequest;
|
||||
|
||||
class PodcastController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
private ICacheFactory $cacheFactory,
|
||||
private IClientService $clientService,
|
||||
private PodcastDataReader $podcastDataReader
|
||||
) {
|
||||
@ -27,10 +29,13 @@ class PodcastController extends Controller
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function index(string $url): JSONResponse {
|
||||
$podcast = null;
|
||||
|
||||
if ($this->cacheFactory->isLocalCacheAvailable()) {
|
||||
try {
|
||||
$podcast = $this->podcastDataReader->tryGetCachedPodcastData($url);
|
||||
} catch (\Exception $e) {
|
||||
$podcast = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($podcast) {
|
||||
@ -41,10 +46,12 @@ class PodcastController extends Controller
|
||||
$feed = $client->get($url);
|
||||
$podcast = PodcastData::parseRssXml((string) $feed->getBody());
|
||||
|
||||
if ($this->cacheFactory->isLocalCacheAvailable()) {
|
||||
try {
|
||||
$this->podcastDataReader->trySetCachedPodcastData($url, $podcast);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
return new JSONResponse($podcast, $feed->getStatusCode());
|
||||
}
|
||||
|
@ -55,22 +55,24 @@ class EpisodeActionReader extends CoreEpisodeActionReader
|
||||
$link = $this->stringOrNull($item->link);
|
||||
|
||||
// Get episode image
|
||||
$image = $this->stringOrNull($item->image->url);
|
||||
|
||||
if (!isset($image) && isset($iTunesItemChildren)) {
|
||||
if (isset($iTunesItemChildren)) {
|
||||
$imageAttributes = $iTunesItemChildren->image->attributes();
|
||||
$image = $this->stringOrNull(isset($imageAttributes) ? (string) $imageAttributes->href : '');
|
||||
}
|
||||
|
||||
if (!isset($image)) {
|
||||
$image = $this->stringOrNull($channel->image->url);
|
||||
}
|
||||
|
||||
if (!isset($image) && isset($iTunesChannelChildren)) {
|
||||
$imageAttributes = $iTunesChannelChildren->image->attributes();
|
||||
$image = $this->stringOrNull(isset($imageAttributes) ? (string) $imageAttributes->href : '');
|
||||
}
|
||||
|
||||
if (!isset($image)) {
|
||||
$image = $this->stringOrNull($item->image->url);
|
||||
}
|
||||
|
||||
if (!isset($image)) {
|
||||
$image = $this->stringOrNull($channel->image->url);
|
||||
}
|
||||
|
||||
if (!isset($image)) {
|
||||
preg_match('/<itunes:image\s+href="([^"]+)"/', $xmlString, $matches);
|
||||
if (count($matches) > 1) {
|
||||
|
10868
package-lock.json
generated
52
package.json
@ -1,38 +1,52 @@
|
||||
{
|
||||
"name": "repod",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"type": "module",
|
||||
"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": "vue-tsc && vite build",
|
||||
"dev": "vite --mode development build",
|
||||
"dev:watch": "vite --mode development build --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"
|
||||
],
|
||||
"prettier": "@nextcloud/prettier-config",
|
||||
"dependencies": {
|
||||
"@nextcloud/axios": "^2.5.0",
|
||||
"@nextcloud/dialogs": "^5.3.1",
|
||||
"@nextcloud/axios": "^2.5.1",
|
||||
"@nextcloud/initial-state": "^2.2.0",
|
||||
"@nextcloud/l10n": "^3.1.0",
|
||||
"@nextcloud/router": "^3.0.1",
|
||||
"@nextcloud/vue": "^8.12.0",
|
||||
"vue": "^2",
|
||||
"vue-material-design-icons": "^5.3.0",
|
||||
"vue-router": "^3",
|
||||
"vuex": "^3"
|
||||
"@nextcloud/vite-config": "^2.2.2",
|
||||
"@nextcloud/vue": "9.0.0-alpha.5",
|
||||
"dompurify": "^3.1.7",
|
||||
"linkify-html": "^4.1.3",
|
||||
"pinia": "^2.2.4",
|
||||
"toastify-js": "^1.12.0",
|
||||
"vite": "^5.4.9",
|
||||
"vite-plugin-vue-devtools": "^7.5.2",
|
||||
"vue": "^3.5.12",
|
||||
"vue-material-design-icons": "^5.3.1",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"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",
|
||||
"@types/toastify-js": "^1.12.3",
|
||||
"@vue/eslint-config-typescript": "^13",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-webpack-plugin": "^4.2.0",
|
||||
"stylelint-webpack-plugin": "^5.0.1"
|
||||
}
|
||||
"eslint-plugin-pinia": "^0.4.1",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "5.5",
|
||||
"vue-eslint-parser": "^9.4.3",
|
||||
"vue-tsc": "^2.1.6"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @nextcloud/browserslist-config"
|
||||
]
|
||||
}
|
||||
|
BIN
screens/discover.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 857 KiB |
Before Width: | Height: | Size: 429 KiB After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 817 KiB |
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 961 KiB |
19
src/App.vue
@ -7,13 +7,15 @@
|
||||
</NcContent>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@nextcloud/dialogs/style.css'
|
||||
<script lang="ts">
|
||||
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.ts'
|
||||
|
||||
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>
|
||||
|
@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<NcAppContent :class="episode ? 'padding' : ''">
|
||||
<NcAppContent :class="{ episode }">
|
||||
<slot />
|
||||
</NcAppContent>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { NcAppContent } from '@nextcloud/vue'
|
||||
import { mapState } from 'pinia'
|
||||
import { usePlayer } from '../../store/player.ts'
|
||||
|
||||
export default {
|
||||
name: 'AppContent',
|
||||
@ -13,15 +15,13 @@ export default {
|
||||
NcAppContent,
|
||||
},
|
||||
computed: {
|
||||
episode() {
|
||||
return this.$store.state.player.episode
|
||||
},
|
||||
...mapState(usePlayer, ['episode']),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.padding {
|
||||
.episode {
|
||||
padding-bottom: 6rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<NcAppNavigation :class="episode ? 'padding' : ''">
|
||||
<NcAppNavigation :class="{ episode }">
|
||||
<slot />
|
||||
<template #list>
|
||||
<slot name="list" />
|
||||
@ -10,8 +10,10 @@
|
||||
</NcAppNavigation>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { NcAppNavigation } from '@nextcloud/vue'
|
||||
import { mapState } from 'pinia'
|
||||
import { usePlayer } from '../../store/player.ts'
|
||||
|
||||
export default {
|
||||
name: 'AppNavigation',
|
||||
@ -19,15 +21,13 @@ export default {
|
||||
NcAppNavigation,
|
||||
},
|
||||
computed: {
|
||||
episode() {
|
||||
return this.$store.state.player.episode
|
||||
},
|
||||
...mapState(usePlayer, ['episode']),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.padding {
|
||||
.episode {
|
||||
padding-bottom: 6rem;
|
||||
}
|
||||
</style>
|
||||
|
34
src/components/Atoms/EmptyContent.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<NcEmptyContent class="empty">
|
||||
<slot />
|
||||
<template #icon>
|
||||
<slot name="icon" />
|
||||
</template>
|
||||
<template #name>
|
||||
<slot name="name" />
|
||||
</template>
|
||||
<template #description>
|
||||
<slot name="description" />
|
||||
</template>
|
||||
<template #action>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { NcEmptyContent } from '@nextcloud/vue'
|
||||
|
||||
export default {
|
||||
name: 'EmptyContent',
|
||||
components: {
|
||||
NcEmptyContent,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@ -2,7 +2,7 @@
|
||||
<NcLoadingIcon class="loading" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { NcLoadingIcon } from '@nextcloud/vue'
|
||||
|
||||
export default {
|
||||
|
@ -1,32 +1,43 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div>
|
||||
<NcAvatar :display-name="name" :is-no-user="true" :size="256" :url="image" />
|
||||
<h2>{{ name }}</h2>
|
||||
<p v-html="strippedDescription" />
|
||||
<div>
|
||||
<NcButton v-if="link" :href="link" target="_blank">
|
||||
<div class="flex">
|
||||
<NcAvatar
|
||||
:display-name="episode.name"
|
||||
:is-no-user="true"
|
||||
:size="256"
|
||||
:url="episode.image" />
|
||||
<h2>{{ episode.name }}</h2>
|
||||
<SafeHtml :source="episode.description || ''" />
|
||||
<div class="flex">
|
||||
<NcButton v-if="episode.link" :href="episode.link" target="_blank">
|
||||
<template #icon>
|
||||
<OpenInNewIcon :size="20" />
|
||||
</template>
|
||||
{{ title }}
|
||||
{{ episode.title }}
|
||||
</NcButton>
|
||||
<NcButton v-if="url" :href="url" target="_blank">
|
||||
<NcButton
|
||||
v-if="episode.url"
|
||||
:download="filenameFromUrl(episode.url)"
|
||||
:href="episode.url"
|
||||
target="_blank">
|
||||
<template #icon>
|
||||
<DownloadIcon :size="20" />
|
||||
</template>
|
||||
{{ t('repod', 'Download') }} {{ size ? `(${episodeFileSize})` : '' }}
|
||||
{{ t('repod', 'Download') }}
|
||||
{{ episode.size ? `(${humanFileSize(episode.size)})` : '' }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { NcAvatar, NcButton } from '@nextcloud/vue'
|
||||
import DownloadIcon from 'vue-material-design-icons/Download.vue'
|
||||
import type { EpisodeInterface } from '../../utils/types.ts'
|
||||
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
|
||||
import { cleanHtml } from '../../utils/text.js'
|
||||
import { humanFileSize } from '../../utils/size.js'
|
||||
import SafeHtml from './SafeHtml.vue'
|
||||
import { filenameFromUrl } from '../../utils/url.ts'
|
||||
import { humanFileSize } from '../../utils/size.ts'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'Modal',
|
||||
@ -35,50 +46,24 @@ export default {
|
||||
NcAvatar,
|
||||
NcButton,
|
||||
OpenInNewIcon,
|
||||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
episode: {
|
||||
type: Object as () => EpisodeInterface,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
episodeFileSize() {
|
||||
return humanFileSize(this.size)
|
||||
},
|
||||
strippedDescription() {
|
||||
return cleanHtml(this.description)
|
||||
},
|
||||
methods: {
|
||||
filenameFromUrl,
|
||||
humanFileSize,
|
||||
t,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
.flex {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
36
src/components/Atoms/SafeHtml.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div v-sanitize="source" class="html" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import dompurify from 'dompurify'
|
||||
import linkifyHtml from 'linkify-html'
|
||||
|
||||
export default {
|
||||
name: 'SafeHtml',
|
||||
directives: {
|
||||
sanitize: {
|
||||
mounted(el, binding) {
|
||||
el.innerHTML = dompurify.sanitize(
|
||||
linkifyHtml(binding.value, {
|
||||
nl2br: true,
|
||||
target: '_blank',
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
source: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.html a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<NcAppNavigationList>
|
||||
<NcAppNavigationList class="list">
|
||||
<NcAppNavigationNewItem
|
||||
:name="t('repod', 'Add a RSS link')"
|
||||
@new-item="addSubscription">
|
||||
@new-item="(url) => $router.push(toFeedUrl(url))">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
@ -10,10 +10,11 @@
|
||||
</NcAppNavigationList>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { NcAppNavigationList, NcAppNavigationNewItem } from '@nextcloud/vue'
|
||||
import PlusIcon from 'vue-material-design-icons/Plus.vue'
|
||||
import { encodeUrl } from '../../utils/url.js'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { toFeedUrl } from '../../utils/url.ts'
|
||||
|
||||
export default {
|
||||
name: 'AddRss',
|
||||
@ -23,15 +24,14 @@ export default {
|
||||
PlusIcon,
|
||||
},
|
||||
methods: {
|
||||
addSubscription(feedUrl) {
|
||||
this.$router.push(encodeUrl(feedUrl))
|
||||
},
|
||||
t,
|
||||
toFeedUrl,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
ul {
|
||||
.list {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -7,7 +7,7 @@
|
||||
:key="feed.link"
|
||||
:details="formatLocaleDate(new Date(feed.fetchedAtUnix * 1000))"
|
||||
:name="feed.title"
|
||||
:to="toUrl(feed.link)">
|
||||
:to="toFeedUrl(feed.link)">
|
||||
<template #icon>
|
||||
<NcAvatar
|
||||
:display-name="feed.author"
|
||||
@ -19,7 +19,7 @@
|
||||
</template>
|
||||
<template #actions>
|
||||
<NcActionButton
|
||||
v-if="!isSubscribed(feed.link)"
|
||||
v-if="!getSubByUrl(feed.link)"
|
||||
:aria-label="t('repod', 'Subscribe')"
|
||||
:name="t('repod', 'Subscribe')"
|
||||
:title="t('repod', 'Subscribe')"
|
||||
@ -34,16 +34,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
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 type { PodcastDataInterface } from '../../utils/types.ts'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { debounce } from '../../utils/debounce.js'
|
||||
import { formatLocaleDate } from '../../utils/time.js'
|
||||
import { formatLocaleDate } from '../../utils/time.ts'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { toUrl } from '../../utils/url.js'
|
||||
import { showError } from '../../utils/toast.ts'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { toFeedUrl } from '../../utils/url.ts'
|
||||
import { useSubscriptions } from '../../store/subscriptions.ts'
|
||||
|
||||
export default {
|
||||
name: 'Search',
|
||||
@ -60,21 +63,28 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
feeds: [],
|
||||
data: () => ({
|
||||
feeds: [] as PodcastDataInterface[],
|
||||
loading: false,
|
||||
}
|
||||
timeout: null as NodeJS.Timeout | null,
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useSubscriptions, ['getSubByUrl']),
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.search()
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
this.timeout = setTimeout(this.search, 200)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useSubscriptions, ['fetch']),
|
||||
formatLocaleDate,
|
||||
toUrl,
|
||||
async addSubscription(url) {
|
||||
t,
|
||||
toFeedUrl,
|
||||
async addSubscription(url: string) {
|
||||
try {
|
||||
await axios.post(
|
||||
generateUrl('/apps/gpoddersync/subscription_change/create'),
|
||||
@ -87,17 +97,13 @@ export default {
|
||||
console.error(e)
|
||||
showError(t('repod', 'Error while adding the feed'))
|
||||
}
|
||||
|
||||
this.$store.dispatch('subscriptions/fetch')
|
||||
this.fetch()
|
||||
},
|
||||
isSubscribed(url) {
|
||||
return this.$store.state.subscriptions.subscriptions.includes(url)
|
||||
},
|
||||
search: debounce(async function value() {
|
||||
async search() {
|
||||
try {
|
||||
this.loading = true
|
||||
const currentSearch = this.value
|
||||
const feeds = await axios.get(
|
||||
const feeds = await axios.get<PodcastDataInterface[]>(
|
||||
generateUrl('/apps/repod/search?q={value}', {
|
||||
value: currentSearch,
|
||||
}),
|
||||
@ -115,7 +121,7 @@ export default {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}, 200),
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<Loading v-if="loading" />
|
||||
<ul v-if="!loading">
|
||||
<li v-for="top in tops" :key="top.link">
|
||||
<router-link :to="toUrl(top.link)">
|
||||
<router-link :to="toFeedUrl(top.link)">
|
||||
<img :src="top.imageUrl" :title="top.author" />
|
||||
</router-link>
|
||||
</li>
|
||||
@ -12,12 +12,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Loading from '../Atoms/Loading.vue'
|
||||
import type { PodcastDataInterface } from '../../utils/types.ts'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { toUrl } from '../../utils/url.js'
|
||||
import { showError } from '../../utils/toast.ts'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { toFeedUrl } from '../../utils/url.ts'
|
||||
|
||||
export default {
|
||||
name: 'Toplist',
|
||||
@ -30,12 +32,10 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: () => ({
|
||||
loading: true,
|
||||
tops: [],
|
||||
}
|
||||
},
|
||||
tops: [] as PodcastDataInterface[],
|
||||
}),
|
||||
computed: {
|
||||
title() {
|
||||
switch (this.type) {
|
||||
@ -51,8 +51,8 @@ export default {
|
||||
async mounted() {
|
||||
try {
|
||||
this.loading = true
|
||||
const tops = await axios.get(
|
||||
generateUrl(`/apps/repod/toplist/${this.type}`),
|
||||
const tops = await axios.get<PodcastDataInterface[]>(
|
||||
generateUrl('/apps/repod/toplist/{type}', { type: this.type }),
|
||||
)
|
||||
this.tops = tops.data
|
||||
} catch (e) {
|
||||
@ -63,7 +63,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toUrl,
|
||||
toFeedUrl,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -1,14 +1,13 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<div class="header">
|
||||
<img class="background" :src="imageUrl" />
|
||||
<img class="background" :src="feed.imageUrl" />
|
||||
<div class="content">
|
||||
<div>
|
||||
<NcAvatar
|
||||
:display-name="author || title"
|
||||
:display-name="feed.author || feed.title"
|
||||
:is-no-user="true"
|
||||
:size="128"
|
||||
:url="imageUrl" />
|
||||
:url="feed.imageUrl" />
|
||||
<a class="feed" :href="url" @click.prevent="copyFeed">
|
||||
<RssIcon :size="20" />
|
||||
<i>{{ t('repod', 'Copy feed') }}</i>
|
||||
@ -16,17 +15,15 @@
|
||||
</div>
|
||||
<div class="inner">
|
||||
<div class="infos">
|
||||
<h2>{{ title }}</h2>
|
||||
<a :href="link" target="_blank">
|
||||
<i>{{ author }}</i>
|
||||
<h2>{{ feed.title }}</h2>
|
||||
<a :href="feed.link" target="_blank">
|
||||
<i>{{ feed.author }}</i>
|
||||
</a>
|
||||
<br /><br />
|
||||
<p>
|
||||
<small v-html="strippedDescription" />
|
||||
</p>
|
||||
<SafeHtml :source="feed.description || ''" />
|
||||
</div>
|
||||
<NcAppNavigationNew
|
||||
v-if="!isSubscribed"
|
||||
v-if="!getSubByUrl(url)"
|
||||
:text="t('repod', 'Subscribe')"
|
||||
@click="addSubscription">
|
||||
<template #icon>
|
||||
@ -38,15 +35,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { NcAppNavigationNew, NcAvatar } from '@nextcloud/vue'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { showError, showSuccess } from '../../utils/toast.ts'
|
||||
import PlusIcon from 'vue-material-design-icons/Plus.vue'
|
||||
import type { PodcastDataInterface } from '../../utils/types.ts'
|
||||
import RssIcon from 'vue-material-design-icons/Rss.vue'
|
||||
import SafeHtml from '../Atoms/SafeHtml.vue'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { cleanHtml } from '../../utils/text.js'
|
||||
import { decodeUrl } from '../../utils/url.js'
|
||||
import { decodeUrl } from '../../utils/url.ts'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { useSubscriptions } from '../../store/subscriptions.ts'
|
||||
|
||||
export default {
|
||||
name: 'Banner',
|
||||
@ -55,41 +56,23 @@ export default {
|
||||
NcAppNavigationNew,
|
||||
PlusIcon,
|
||||
RssIcon,
|
||||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
author: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
imageUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
feed: {
|
||||
type: Object as () => PodcastDataInterface,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(useSubscriptions, ['getSubByUrl']),
|
||||
url() {
|
||||
return decodeUrl(this.$route.params.url)
|
||||
},
|
||||
isSubscribed() {
|
||||
return this.$store.state.subscriptions.subscriptions.includes(this.url)
|
||||
},
|
||||
strippedDescription() {
|
||||
return cleanHtml(this.description)
|
||||
return decodeUrl(this.$route.params.url as string)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useSubscriptions, ['fetch']),
|
||||
t,
|
||||
async addSubscription() {
|
||||
try {
|
||||
await axios.post(
|
||||
@ -103,12 +86,11 @@ export default {
|
||||
console.error(e)
|
||||
showError(t('repod', 'Error while adding the feed'))
|
||||
}
|
||||
|
||||
this.$store.dispatch('subscriptions/fetch')
|
||||
this.fetch()
|
||||
},
|
||||
copyFeed() {
|
||||
window.navigator.clipboard.writeText(this.url)
|
||||
showSuccess(t('repod', "Feed's link copied to the clipboard"))
|
||||
showSuccess(t('repod', 'Link copied to the clipboard'))
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -147,11 +129,13 @@ export default {
|
||||
}
|
||||
|
||||
.infos {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
|
222
src/components/Feed/Episode.vue
Normal file
@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<NcListItem
|
||||
:active="isCurrentEpisode(episode)"
|
||||
class="episode"
|
||||
:details="
|
||||
!oneLine && episode.pubDate
|
||||
? 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="episode.action && isListening(episode) && !oneLine"
|
||||
class="progress"
|
||||
:value="(episode.action.position * 100) / episode.action.total" />
|
||||
</template>
|
||||
<template #subname>
|
||||
{{ episode.duration }}
|
||||
</template>
|
||||
</NcListItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
NcActionButton,
|
||||
NcActionLink,
|
||||
NcActions,
|
||||
NcAvatar,
|
||||
NcListItem,
|
||||
NcModal,
|
||||
NcProgressBar,
|
||||
} from '@nextcloud/vue'
|
||||
import {
|
||||
durationToSeconds,
|
||||
formatEpisodeTimestamp,
|
||||
formatLocaleDate,
|
||||
} from '../../utils/time.ts'
|
||||
import { hasEnded, isListening } from '../../utils/status.ts'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import DownloadIcon from 'vue-material-design-icons/Download.vue'
|
||||
import type { EpisodeInterface } from '../../utils/types.ts'
|
||||
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.ts'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError } from '../../utils/toast.ts'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { usePlayer } from '../../store/player.ts'
|
||||
|
||||
export default {
|
||||
name: 'Episode',
|
||||
components: {
|
||||
DownloadIcon,
|
||||
Modal,
|
||||
NcActionButton,
|
||||
NcActionLink,
|
||||
NcActions,
|
||||
NcAvatar,
|
||||
NcListItem,
|
||||
NcModal,
|
||||
NcProgressBar,
|
||||
OpenInNewIcon,
|
||||
PlayIcon,
|
||||
PlaylistPlayIcon,
|
||||
PlaylistRemoveIcon,
|
||||
StopIcon,
|
||||
},
|
||||
props: {
|
||||
episode: {
|
||||
type: Object as () => EpisodeInterface,
|
||||
required: true,
|
||||
},
|
||||
oneLine: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
loading: false,
|
||||
modalEpisode: null as EpisodeInterface | null,
|
||||
}),
|
||||
computed: {
|
||||
...mapState(usePlayer, { playerEpisode: 'episode' }),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(usePlayer, ['load']),
|
||||
filenameFromUrl,
|
||||
formatLocaleDate,
|
||||
hasEnded,
|
||||
isListening,
|
||||
t,
|
||||
isCurrentEpisode(episode: EpisodeInterface) {
|
||||
return this.playerEpisode?.url === episode.url
|
||||
},
|
||||
async markAs(episode: EpisodeInterface, read: boolean) {
|
||||
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>
|
||||
|
||||
<style>
|
||||
.episode .list-item-content__name {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.episode .list-item-content__subname {
|
||||
flex-basis: auto;
|
||||
flex-grow: 0;
|
||||
}
|
||||
</style>
|
@ -2,212 +2,82 @@
|
||||
<div>
|
||||
<Loading v-if="loading" />
|
||||
<ul v-if="!loading">
|
||||
<NcListItem
|
||||
<Episode
|
||||
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"
|
||||
:name="episode.name"
|
||||
:title="episode.description"
|
||||
@click="modalEpisode = episode">
|
||||
<template #actions>
|
||||
<NcActionButton
|
||||
v-if="!isCurrentEpisode(episode)"
|
||||
:aria-label="t('repod', 'Play')"
|
||||
:name="t('repod', 'Play')"
|
||||
:title="t('repod', 'Play')"
|
||||
@click="load(episode)">
|
||||
<template #icon>
|
||||
<PlayIcon :size="20" />
|
||||
</template>
|
||||
</NcActionButton>
|
||||
<NcActionButton
|
||||
v-if="isCurrentEpisode(episode)"
|
||||
:aria-label="t('repod', 'Stop')"
|
||||
:name="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"
|
||||
:href="episode.url"
|
||||
:name="t('repod', 'Download')"
|
||||
target="_blank"
|
||||
:title="t('repod', 'Download')">
|
||||
<template #icon>
|
||||
<DownloadIcon :size="20" />
|
||||
</template>
|
||||
</NcActionLink>
|
||||
</NcActions>
|
||||
</template>
|
||||
<template #icon>
|
||||
<NcAvatar
|
||||
:display-name="episode.name"
|
||||
:is-no-user="true"
|
||||
:url="episode.image" />
|
||||
</template>
|
||||
<template #indicator>
|
||||
<NcProgressBar
|
||||
v-if="isListening(episode)"
|
||||
class="progress"
|
||||
:value="
|
||||
(episode.action.position * 100) / episode.action.total
|
||||
" />
|
||||
</template>
|
||||
<template #subname>
|
||||
{{ episode.duration }}
|
||||
</template>
|
||||
</NcListItem>
|
||||
:episode="episode"
|
||||
:url="url" />
|
||||
</ul>
|
||||
<NcModal v-if="modalEpisode" @close="modalEpisode = null">
|
||||
<Modal
|
||||
:description="modalEpisode.description"
|
||||
:image="modalEpisode.image"
|
||||
:link="modalEpisode.link"
|
||||
:name="modalEpisode.name"
|
||||
:size="modalEpisode.size"
|
||||
:title="modalEpisode.title"
|
||||
:url="modalEpisode.url" />
|
||||
</NcModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
NcActionButton,
|
||||
NcActionLink,
|
||||
NcActions,
|
||||
NcAvatar,
|
||||
NcListItem,
|
||||
NcModal,
|
||||
NcProgressBar,
|
||||
} from '@nextcloud/vue'
|
||||
import {
|
||||
durationToSeconds,
|
||||
formatEpisodeTimestamp,
|
||||
formatLocaleDate,
|
||||
} from '../../utils/time.js'
|
||||
import DownloadIcon from 'vue-material-design-icons/Download.vue'
|
||||
import { EventBus } from '../../store/bus.js'
|
||||
<script lang="ts">
|
||||
import { hasEnded, isListening } from '../../utils/status.ts'
|
||||
import Episode from './Episode.vue'
|
||||
import type { EpisodeInterface } from '../../utils/types.ts'
|
||||
import Loading from '../Atoms/Loading.vue'
|
||||
import Modal from '../Atoms/Modal.vue'
|
||||
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
|
||||
import PlayIcon from 'vue-material-design-icons/Play.vue'
|
||||
import PlaylistPlayIcon from 'vue-material-design-icons/PlaylistPlay.vue'
|
||||
import PlaylistRemoveIcon from 'vue-material-design-icons/PlaylistRemove.vue'
|
||||
import StopIcon from 'vue-material-design-icons/Stop.vue'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { decodeUrl } from '../../utils/url.js'
|
||||
import { decodeUrl } from '../../utils/url.ts'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { mapState } from 'pinia'
|
||||
import { showError } from '../../utils/toast.ts'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { usePlayer } from '../../store/player.ts'
|
||||
import { useSettings } from '../../store/settings.ts'
|
||||
|
||||
export default {
|
||||
name: 'Episodes',
|
||||
components: {
|
||||
DownloadIcon,
|
||||
Episode,
|
||||
Loading,
|
||||
Modal,
|
||||
NcActionButton,
|
||||
NcActionLink,
|
||||
NcActions,
|
||||
NcAvatar,
|
||||
NcListItem,
|
||||
NcModal,
|
||||
NcProgressBar,
|
||||
OpenInNewIcon,
|
||||
PlayIcon,
|
||||
PlaylistPlayIcon,
|
||||
PlaylistRemoveIcon,
|
||||
StopIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
episodes: [],
|
||||
data: () => ({
|
||||
episodes: [] as EpisodeInterface[],
|
||||
loading: true,
|
||||
loadingAction: false,
|
||||
modalEpisode: null,
|
||||
}
|
||||
},
|
||||
}),
|
||||
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)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!this.filters.listening && this.isListening(episode)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!this.filters.unlistened && !this.isListening(episode)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
},
|
||||
url() {
|
||||
return decodeUrl(this.$route.params.url)
|
||||
return decodeUrl(this.$route.params.url as string)
|
||||
},
|
||||
},
|
||||
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
|
||||
const episodes = await axios.get(
|
||||
const episodes = await axios.get<EpisodeInterface[]>(
|
||||
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),
|
||||
(a, b) =>
|
||||
new Date(b.pubDate?.date || '').getTime() -
|
||||
new Date(a.pubDate?.date || '').getTime(),
|
||||
)
|
||||
EventBus.$on('updateEpisodesList', this.updateList)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
showError(t('repod', 'Could not fetch episodes'))
|
||||
@ -215,72 +85,9 @@ export default {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
EventBus.$off('updateEpisodesList')
|
||||
},
|
||||
methods: {
|
||||
formatLocaleDate,
|
||||
hasEnded(episode) {
|
||||
return (
|
||||
episode.action &&
|
||||
episode.action.position > 0 &&
|
||||
episode.action.total > 0 &&
|
||||
episode.action.position >= episode.action.total
|
||||
)
|
||||
},
|
||||
isCurrentEpisode(episode) {
|
||||
return this.currentEpisode && this.currentEpisode.url === episode.url
|
||||
},
|
||||
isListening(episode) {
|
||||
return (
|
||||
episode.action &&
|
||||
episode.action.action.toLowerCase() === 'play' &&
|
||||
!this.hasEnded(episode)
|
||||
)
|
||||
},
|
||||
load(episode) {
|
||||
this.$store.dispatch('player/load', 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],
|
||||
)
|
||||
this.updateList(episode)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
showError(t('repod', 'Could not change the status of the episode'))
|
||||
} finally {
|
||||
this.loadingAction = false
|
||||
}
|
||||
},
|
||||
updateList(episode) {
|
||||
this.episodes = this.episodes.map((e) =>
|
||||
e.url === episode.url ? episode : e,
|
||||
)
|
||||
},
|
||||
hasEnded,
|
||||
isListening,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ended {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
</style>
|
||||
|
105
src/components/Feed/Favorite.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<NcGuestContent class="guest">
|
||||
<Loading v-if="!feed.data" />
|
||||
<NcAvatar
|
||||
v-if="feed.data"
|
||||
class="avatar"
|
||||
:display-name="feed.data.author || feed.data.title"
|
||||
:is-no-user="true"
|
||||
:size="222"
|
||||
:url="feed.data.imageUrl" />
|
||||
<div v-if="feed.data" class="list">
|
||||
<h2 class="title">{{ feed.data.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="feed.metrics.url" />
|
||||
</ul>
|
||||
</div>
|
||||
</NcGuestContent>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { EpisodeInterface, SubscriptionInterface } from '../../utils/types.ts'
|
||||
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.ts'
|
||||
import { showError } from '../../utils/toast.ts'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'Favorite',
|
||||
components: {
|
||||
Episode,
|
||||
Loading,
|
||||
NcAvatar,
|
||||
NcGuestContent,
|
||||
},
|
||||
props: {
|
||||
feed: {
|
||||
type: Object as () => SubscriptionInterface,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
episodes: [] as EpisodeInterface[],
|
||||
loading: true,
|
||||
}),
|
||||
async mounted() {
|
||||
try {
|
||||
this.loading = true
|
||||
const episodes = await axios.get<EpisodeInterface[]>(
|
||||
generateUrl('/apps/repod/episodes/list?url={url}', {
|
||||
url: this.feed.metrics.url,
|
||||
}),
|
||||
)
|
||||
this.episodes = [...episodes.data]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.pubDate?.date || '').getTime() -
|
||||
new Date(a.pubDate?.date || '').getTime(),
|
||||
)
|
||||
.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;
|
||||
margin: 20px !important;
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.avatar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -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" />
|
||||
@ -13,13 +13,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Controls from './Controls.vue'
|
||||
import Infos from './Infos.vue'
|
||||
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.ts'
|
||||
|
||||
export default {
|
||||
name: 'Bar',
|
||||
@ -32,9 +34,7 @@ export default {
|
||||
Volume,
|
||||
},
|
||||
computed: {
|
||||
player() {
|
||||
return this.$store.state.player
|
||||
},
|
||||
...mapState(usePlayer, ['episode', 'loaded']),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -78,7 +78,7 @@ export default {
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.infos {
|
||||
flex: 2;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timer,
|
||||
|
@ -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>
|
||||
<script lang="ts">
|
||||
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.ts'
|
||||
|
||||
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>
|
||||
|
@ -1,28 +1,23 @@
|
||||
<template>
|
||||
<div class="root">
|
||||
<div v-if="episode && podcastUrl" class="root">
|
||||
<strong class="pointer" @click="modal = true">
|
||||
{{ player.episode.name }}
|
||||
{{ episode.name }}
|
||||
</strong>
|
||||
<router-link :to="hash">
|
||||
<i>{{ player.episode.title }}</i>
|
||||
<router-link :to="toFeedUrl(podcastUrl)">
|
||||
<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" />
|
||||
<Modal :episode="episode" />
|
||||
</NcModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Modal from '../Atoms/Modal.vue'
|
||||
import { NcModal } from '@nextcloud/vue'
|
||||
import { toUrl } from '../../utils/url.js'
|
||||
import { mapState } from 'pinia'
|
||||
import { toFeedUrl } from '../../utils/url.ts'
|
||||
import { usePlayer } from '../../store/player.ts'
|
||||
|
||||
export default {
|
||||
name: 'Infos',
|
||||
@ -30,18 +25,14 @@ export default {
|
||||
Modal,
|
||||
NcModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: () => ({
|
||||
modal: false,
|
||||
}
|
||||
},
|
||||
}),
|
||||
computed: {
|
||||
player() {
|
||||
return this.$store.state.player
|
||||
},
|
||||
hash() {
|
||||
return toUrl(this.player.podcastUrl)
|
||||
...mapState(usePlayer, ['episode', 'podcastUrl']),
|
||||
},
|
||||
methods: {
|
||||
toFeedUrl,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -1,20 +1,27 @@
|
||||
<template>
|
||||
<input
|
||||
v-if="duration"
|
||||
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(parseInt((event.target as HTMLInputElement).value))
|
||||
" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { usePlayer } from '../../store/player.ts'
|
||||
|
||||
export default {
|
||||
name: 'ProgressBar',
|
||||
computed: {
|
||||
player() {
|
||||
return this.$store.state.player
|
||||
...mapState(usePlayer, ['duration', 'currentTime']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(usePlayer, ['seek']),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<span>{{ formatTimer(new Date(player.currentTime * 1000)) }}</span>
|
||||
<div v-if="currentTime && duration" class="root">
|
||||
<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'
|
||||
<script lang="ts">
|
||||
import { formatTimer } from '../../utils/time.ts'
|
||||
import { mapState } from 'pinia'
|
||||
import { usePlayer } from '../../store/player.ts'
|
||||
|
||||
export default {
|
||||
name: 'Timer',
|
||||
computed: {
|
||||
player() {
|
||||
return this.$store.state.player
|
||||
},
|
||||
...mapState(usePlayer, ['duration', 'currentTime']),
|
||||
},
|
||||
methods: {
|
||||
formatTimer,
|
||||
@ -23,7 +23,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
.root {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
|
@ -1,42 +1,45 @@
|
||||
<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"
|
||||
:value="volume"
|
||||
@change="
|
||||
(event) => $store.dispatch('player/volume', event.target.value)
|
||||
(event) =>
|
||||
setVolume(parseInt((event.target as HTMLInputElement).value))
|
||||
" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
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.ts'
|
||||
|
||||
export default {
|
||||
name: 'Volume',
|
||||
@ -46,20 +49,17 @@ export default {
|
||||
VolumeMediumIcon,
|
||||
VolumeMuteIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: () => ({
|
||||
volumeMuted: 0,
|
||||
}
|
||||
},
|
||||
}),
|
||||
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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -8,10 +8,11 @@
|
||||
</NcAppNavigationItem>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import ExportIcon from 'vue-material-design-icons/Export.vue'
|
||||
import { NcAppNavigationItem } from '@nextcloud/vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'Export',
|
||||
@ -21,6 +22,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
generateUrl,
|
||||
t,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -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>
|
||||
@ -49,10 +39,13 @@
|
||||
</NcAppNavigationItem>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
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 { t } from '@nextcloud/l10n'
|
||||
import { useSettings } from '../../store/settings.ts'
|
||||
|
||||
export default {
|
||||
name: 'Filters',
|
||||
@ -63,6 +56,7 @@ export default {
|
||||
NcActionCheckbox,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useSettings, ['filters']),
|
||||
all() {
|
||||
return (
|
||||
this.filters.listened &&
|
||||
@ -70,12 +64,10 @@ export default {
|
||||
this.filters.unlistened
|
||||
)
|
||||
},
|
||||
filters() {
|
||||
return this.$store.state.settings.filters
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('settings/fetch')
|
||||
methods: {
|
||||
...mapActions(useSettings, ['setFilters']),
|
||||
t,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -29,12 +29,13 @@
|
||||
</NcAppNavigationItem>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { NcAppNavigationItem, NcModal } from '@nextcloud/vue'
|
||||
import ImportIcon from 'vue-material-design-icons/Import.vue'
|
||||
import Loading from '../Atoms/Loading.vue'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'Import',
|
||||
@ -44,19 +45,19 @@ export default {
|
||||
NcAppNavigationItem,
|
||||
NcModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: () => ({
|
||||
loading: false,
|
||||
modal: false,
|
||||
}
|
||||
},
|
||||
}),
|
||||
methods: {
|
||||
generateUrl,
|
||||
async importOpml(event) {
|
||||
t,
|
||||
async importOpml(event: Event) {
|
||||
try {
|
||||
const formData = new FormData(event.target)
|
||||
this.importLoading = true
|
||||
await axios.post(event.target.action, formData)
|
||||
const target = event.target as HTMLFormElement
|
||||
const formData = new FormData(target)
|
||||
this.loading = true
|
||||
await axios.post(target.action, formData)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
|
@ -8,9 +8,10 @@
|
||||
</NcAppNavigationItem>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { NcAppNavigationItem } from '@nextcloud/vue'
|
||||
import StarIcon from 'vue-material-design-icons/Star.vue'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'Rate',
|
||||
@ -18,5 +19,8 @@ export default {
|
||||
NcAppNavigationItem,
|
||||
StarIcon,
|
||||
},
|
||||
methods: {
|
||||
t,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -8,7 +8,7 @@
|
||||
</NcAppNavigationSettings>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Export from './Export.vue'
|
||||
import Filters from './Filters.vue'
|
||||
import Import from './Import.vue'
|
||||
|
@ -3,27 +3,28 @@
|
||||
<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>
|
||||
<script lang="ts">
|
||||
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 { t } from '@nextcloud/l10n'
|
||||
import { usePlayer } from '../../store/player.ts'
|
||||
|
||||
export default {
|
||||
name: 'Speed',
|
||||
@ -37,17 +38,14 @@ export default {
|
||||
SpeedometerSlowIcon,
|
||||
},
|
||||
computed: {
|
||||
player() {
|
||||
return this.$store.state.player
|
||||
},
|
||||
...mapState(usePlayer, ['rate']),
|
||||
},
|
||||
methods: {
|
||||
changeRate(diff) {
|
||||
const newRate = (this.player.rate + diff).toPrecision(2)
|
||||
this.$store.dispatch(
|
||||
'player/rate',
|
||||
newRate > 0 ? newRate : this.player.rate,
|
||||
)
|
||||
...mapActions(usePlayer, ['setRate']),
|
||||
t,
|
||||
changeRate(diff: number) {
|
||||
const newRate = parseFloat((this.rate + diff).toPrecision(2))
|
||||
this.setRate(newRate > 0 ? newRate : this.rate)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<NcAppNavigationItem
|
||||
:loading="loading"
|
||||
:name="feed ? feed.title : url"
|
||||
:to="hash">
|
||||
<template #actions>
|
||||
<NcActionButton
|
||||
:aria-label="t(`core`, 'Delete')"
|
||||
:name="t(`core`, 'Delete')"
|
||||
:title="t(`core`, 'Delete')"
|
||||
@click="deleteSubscription">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
</NcActionButton>
|
||||
</template>
|
||||
<template #icon>
|
||||
<NcAvatar
|
||||
v-if="feed"
|
||||
:display-name="feed.author || feed.title"
|
||||
:is-no-user="true"
|
||||
:url="feed.imageUrl" />
|
||||
<AlertIcon v-if="failed" />
|
||||
</template>
|
||||
</NcAppNavigationItem>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NcActionButton, NcAppNavigationItem, NcAvatar } from '@nextcloud/vue'
|
||||
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 { toUrl } from '../../utils/url.js'
|
||||
|
||||
export default {
|
||||
name: 'Item',
|
||||
components: {
|
||||
AlertIcon,
|
||||
DeleteIcon,
|
||||
NcActionButton,
|
||||
NcAppNavigationItem,
|
||||
NcAvatar,
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
failed: false,
|
||||
loading: true,
|
||||
feed: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hash() {
|
||||
return toUrl(this.url)
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
const podcastData = await axios.get(
|
||||
generateUrl(
|
||||
'/apps/gpoddersync/personal_settings/podcast_data?url={url}',
|
||||
{
|
||||
url: this.url,
|
||||
},
|
||||
),
|
||||
)
|
||||
this.feed = podcastData.data.data
|
||||
} catch (e) {
|
||||
this.failed = true
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async deleteSubscription() {
|
||||
if (
|
||||
confirm(
|
||||
t('repod', 'Are you sure you want to delete this subscription?'),
|
||||
)
|
||||
) {
|
||||
try {
|
||||
this.loading = true
|
||||
await axios.post(
|
||||
generateUrl('/apps/gpoddersync/subscription_change/create'),
|
||||
{ add: [], remove: [this.url] },
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
showError(t('repod', 'Error while removing the feed'))
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.$store.dispatch('subscriptions/fetch')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
148
src/components/Sidebar/Subscription.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<NcAppNavigationItem
|
||||
:loading="loading"
|
||||
:name="feed?.data?.title || url"
|
||||
:to="toFeedUrl(url)">
|
||||
<template #actions>
|
||||
<NcActionButton
|
||||
:aria-label="t('repod', 'Favorite')"
|
||||
:model-value="feed?.isFavorite"
|
||||
:name="t('repod', 'Favorite')"
|
||||
:title="t('repod', 'Favorite')"
|
||||
@update:modelValue="switchFavorite($event)">
|
||||
<template #icon>
|
||||
<StarPlusIcon v-if="!feed?.isFavorite" :size="20" />
|
||||
<StarRemoveIcon v-if="feed?.isFavorite" :size="20" />
|
||||
</template>
|
||||
</NcActionButton>
|
||||
<NcActionButton
|
||||
:aria-label="t(`core`, 'Delete')"
|
||||
:name="t(`core`, 'Delete')"
|
||||
:title="t(`core`, 'Delete')"
|
||||
@click="deleteSubscription">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
</NcActionButton>
|
||||
</template>
|
||||
<template #icon>
|
||||
<NcAvatar
|
||||
:display-name="feed?.data?.author || feed?.data?.title"
|
||||
:is-no-user="true"
|
||||
:url="feed?.data?.imageUrl" />
|
||||
<StarIcon v-if="feed?.isFavorite" class="star" :size="20" />
|
||||
<AlertIcon v-if="failed" />
|
||||
</template>
|
||||
</NcAppNavigationItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { NcActionButton, NcAppNavigationItem, NcAvatar } from '@nextcloud/vue'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import AlertIcon from 'vue-material-design-icons/Alert.vue'
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
|
||||
import type { PersonalSettingsPodcastDataInterface } from '../../utils/types.ts'
|
||||
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 { generateUrl } from '@nextcloud/router'
|
||||
import { showError } from '../../utils/toast.ts'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { toFeedUrl } from '../../utils/url.ts'
|
||||
import { useSubscriptions } from '../../store/subscriptions.ts'
|
||||
|
||||
export default {
|
||||
name: 'Subscription',
|
||||
components: {
|
||||
AlertIcon,
|
||||
DeleteIcon,
|
||||
NcActionButton,
|
||||
NcAppNavigationItem,
|
||||
NcAvatar,
|
||||
StarIcon,
|
||||
StarPlusIcon,
|
||||
StarRemoveIcon,
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
failed: false,
|
||||
loading: true,
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useSubscriptions, ['subs']),
|
||||
feed() {
|
||||
return this.subs.find((sub) => sub.metrics.url === this.url)
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
const podcastData =
|
||||
await axios.get<PersonalSettingsPodcastDataInterface>(
|
||||
generateUrl(
|
||||
'/apps/gpoddersync/personal_settings/podcast_data?url={url}',
|
||||
{
|
||||
url: this.url,
|
||||
},
|
||||
),
|
||||
)
|
||||
this.addMetadatas(this.url, podcastData.data.data)
|
||||
} catch (e) {
|
||||
this.failed = true
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useSubscriptions, ['fetch', 'addMetadatas', 'setFavorite']),
|
||||
t,
|
||||
toFeedUrl,
|
||||
async deleteSubscription() {
|
||||
if (
|
||||
confirm(
|
||||
t('repod', 'Are you sure you want to delete this subscription?'),
|
||||
)
|
||||
) {
|
||||
try {
|
||||
this.loading = true
|
||||
await axios.post(
|
||||
generateUrl('/apps/gpoddersync/subscription_change/create'),
|
||||
{ add: [], remove: [this.url] },
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
showError(t('repod', 'Error while removing the feed'))
|
||||
} finally {
|
||||
this.setFavorite(this.url, false)
|
||||
this.loading = false
|
||||
this.fetch()
|
||||
}
|
||||
}
|
||||
},
|
||||
switchFavorite(value: boolean) {
|
||||
if (value) {
|
||||
if (this.subs.filter((sub) => sub.isFavorite).length >= 10) {
|
||||
showError(t('repod', 'You can only have 10 favorites'))
|
||||
return
|
||||
}
|
||||
}
|
||||
this.setFavorite(this.url, value)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.star {
|
||||
bottom: 2px;
|
||||
color: yellow;
|
||||
left: 22px;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
@ -2,7 +2,7 @@
|
||||
<AppNavigation>
|
||||
<template #list>
|
||||
<NcAppContentList>
|
||||
<router-link to="/">
|
||||
<router-link to="/discover">
|
||||
<NcAppNavigationNew :text="t('repod', 'Add a podcast')">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
@ -11,10 +11,14 @@
|
||||
</router-link>
|
||||
<Loading v-if="loading" />
|
||||
<NcAppNavigationList v-if="!loading">
|
||||
<Item
|
||||
v-for="subscriptionUrl of subscriptions"
|
||||
:key="subscriptionUrl"
|
||||
:url="subscriptionUrl" />
|
||||
<Subscription
|
||||
v-for="sub of subs.filter((sub) => sub.isFavorite)"
|
||||
:key="sub.metrics.url"
|
||||
:url="sub.metrics.url" />
|
||||
<Subscription
|
||||
v-for="sub of subs.filter((sub) => !sub.isFavorite)"
|
||||
:key="sub.metrics.url"
|
||||
:url="sub.metrics.url" />
|
||||
</NcAppNavigationList>
|
||||
</NcAppContentList>
|
||||
</template>
|
||||
@ -24,44 +28,43 @@
|
||||
</AppNavigation>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import {
|
||||
NcAppContentList,
|
||||
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 Subscription from './Subscription.vue'
|
||||
import { showError } from '../../utils/toast.ts'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { useSubscriptions } from '../../store/subscriptions.ts'
|
||||
|
||||
export default {
|
||||
name: 'Subscriptions',
|
||||
components: {
|
||||
AppNavigation,
|
||||
Item,
|
||||
Loading,
|
||||
NcAppContentList,
|
||||
NcAppNavigationList,
|
||||
NcAppNavigationNew,
|
||||
PlusIcon,
|
||||
Settings,
|
||||
Subscription,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: () => ({
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
}),
|
||||
computed: {
|
||||
subscriptions() {
|
||||
return this.$store.state.subscriptions.subscriptions
|
||||
},
|
||||
...mapState(useSubscriptions, ['subs']),
|
||||
},
|
||||
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 +72,9 @@ export default {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useSubscriptions, ['fetch']),
|
||||
t,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
13
src/main.js
@ -1,13 +0,0 @@
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import App from './App.vue'
|
||||
import Vue from 'vue'
|
||||
import router from './router.js'
|
||||
import store from './store/main.js'
|
||||
|
||||
Vue.mixin({ methods: { t, n } })
|
||||
|
||||
const View = Vue.extend(App)
|
||||
new View({
|
||||
router,
|
||||
store,
|
||||
}).$mount('#content')
|
11
src/main.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import App from './App.vue'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import router from './router.ts'
|
||||
|
||||
const Vue = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
Vue.use(pinia)
|
||||
Vue.use(router)
|
||||
Vue.mount('#content')
|
@ -1,23 +0,0 @@
|
||||
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'),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: Discover,
|
||||
},
|
||||
{
|
||||
path: '/:url',
|
||||
component: Feed,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
25
src/router.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Discover from './views/Discover.vue'
|
||||
import Feed from './views/Feed.vue'
|
||||
import Home from './views/Home.vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(generateUrl('apps/repod')),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: '/discover',
|
||||
component: Discover,
|
||||
},
|
||||
{
|
||||
path: '/feed/:url',
|
||||
component: Feed,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
@ -1,3 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export const EventBus = new Vue()
|
@ -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
|
@ -1,147 +0,0 @@
|
||||
import { EventBus } from './bus.js'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { decodeUrl } from '../utils/url.js'
|
||||
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: {
|
||||
currentTime: null,
|
||||
duration: null,
|
||||
episode: null,
|
||||
loaded: false,
|
||||
paused: null,
|
||||
podcastUrl: null,
|
||||
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)
|
||||
try {
|
||||
const action = await axios.get(
|
||||
generateUrl('/apps/repod/episodes/action?url={url}', {
|
||||
url: episode.url,
|
||||
}),
|
||||
)
|
||||
context.commit('action', action.data)
|
||||
} catch {}
|
||||
},
|
||||
pause: (context) => {
|
||||
audio.pause()
|
||||
context.commit('paused', true)
|
||||
context.dispatch('time')
|
||||
},
|
||||
play: (context) => {
|
||||
audio.play()
|
||||
context.commit('paused', false)
|
||||
context.commit('started', audio.currentTime)
|
||||
},
|
||||
seek: (context, currentTime) => {
|
||||
audio.currentTime = currentTime
|
||||
context.dispatch('time')
|
||||
},
|
||||
stop: (context) => {
|
||||
context.dispatch('pause')
|
||||
context.commit('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,
|
||||
action: 'play',
|
||||
timestamp: formatEpisodeTimestamp(new Date()),
|
||||
started: Math.round(context.state.started),
|
||||
position: Math.round(audio.currentTime),
|
||||
total: Math.round(audio.duration),
|
||||
}
|
||||
axios.post(generateUrl('/apps/gpoddersync/episode_action/create'), [
|
||||
episode.action,
|
||||
])
|
||||
EventBus.$emit('updateEpisodesList', episode)
|
||||
},
|
||||
volume: (_, volume) => {
|
||||
audio.volume = volume
|
||||
},
|
||||
rate: (_, rate) => {
|
||||
audio.playbackRate = rate
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
if (player.state.paused === false) {
|
||||
store.dispatch('player/time')
|
||||
}
|
||||
}, 40000)
|
111
src/store/player.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import type { EpisodeActionInterface, EpisodeInterface } from '../utils/types.ts'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { formatEpisodeTimestamp } from '../utils/time.ts'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
const audio = new Audio()
|
||||
|
||||
export const usePlayer = defineStore('player', {
|
||||
state: () => ({
|
||||
currentTime: null as number | null,
|
||||
duration: null as number | null,
|
||||
episode: null as EpisodeInterface | null,
|
||||
loaded: false,
|
||||
paused: true,
|
||||
podcastUrl: null as string | null,
|
||||
volume: 1,
|
||||
rate: 1,
|
||||
started: 0,
|
||||
}),
|
||||
actions: {
|
||||
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: EpisodeInterface | null, podcastUrl?: string) {
|
||||
this.episode = episode
|
||||
this.podcastUrl = podcastUrl || null
|
||||
|
||||
if (this.episode?.url) {
|
||||
audio.src = this.episode.url
|
||||
audio.load()
|
||||
|
||||
try {
|
||||
const action = await axios.get<EpisodeActionInterface>(
|
||||
generateUrl('/apps/repod/episodes/action?url={url}', {
|
||||
url: this.episode.url,
|
||||
}),
|
||||
)
|
||||
|
||||
this.episode.action = action.data
|
||||
} catch {}
|
||||
|
||||
if (
|
||||
this.episode.action &&
|
||||
this.episode.action.position < this.episode.action.total
|
||||
) {
|
||||
audio.currentTime = this.episode.action.position
|
||||
this.started = audio.currentTime
|
||||
}
|
||||
|
||||
audio.play()
|
||||
} else {
|
||||
this.loaded = false
|
||||
this.podcastUrl = null
|
||||
audio.src = ''
|
||||
}
|
||||
},
|
||||
pause() {
|
||||
audio.pause()
|
||||
this.paused = true
|
||||
this.time()
|
||||
},
|
||||
play() {
|
||||
audio.play()
|
||||
this.paused = false
|
||||
this.started = audio.currentTime
|
||||
},
|
||||
seek(currentTime: number) {
|
||||
audio.currentTime = currentTime
|
||||
this.time()
|
||||
},
|
||||
stop() {
|
||||
this.pause()
|
||||
this.episode = null
|
||||
},
|
||||
time() {
|
||||
if (!this.podcastUrl || !this.episode?.url) {
|
||||
return
|
||||
}
|
||||
|
||||
this.episode.action = {
|
||||
podcast: this.podcastUrl,
|
||||
episode: this.episode.url,
|
||||
guid: this.episode.guid,
|
||||
action: 'play',
|
||||
timestamp: formatEpisodeTimestamp(new Date()),
|
||||
started: Math.round(this.started),
|
||||
position: Math.round(audio.currentTime),
|
||||
total: Math.round(audio.duration),
|
||||
}
|
||||
|
||||
axios.post(generateUrl('/apps/gpoddersync/episode_action/create'), [
|
||||
this.episode.action,
|
||||
])
|
||||
},
|
||||
setVolume(volume: number) {
|
||||
audio.volume = volume
|
||||
},
|
||||
setRate(rate: number) {
|
||||
audio.playbackRate = rate
|
||||
},
|
||||
},
|
||||
})
|
@ -1,28 +0,0 @@
|
||||
import { getCookie, setCookie } from '../utils/cookies.js'
|
||||
|
||||
export const settings = {
|
||||
namespaced: true,
|
||||
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
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
37
src/store/settings.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { getCookie, setCookie } from '../utils/cookies.ts'
|
||||
import type { FiltersInterface } from '../utils/types.ts'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useSettings = defineStore('settings', {
|
||||
state: () => {
|
||||
try {
|
||||
const filters = JSON.parse(getCookie('repod.filters') || '{}') || {}
|
||||
|
||||
if (!filters.length) {
|
||||
throw new Error('Empty cookie')
|
||||
}
|
||||
|
||||
return {
|
||||
filters: {
|
||||
listened: filters.listened,
|
||||
listening: filters.listening,
|
||||
unlistened: filters.unlistened,
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
filters: {
|
||||
listened: true,
|
||||
listening: true,
|
||||
unlistened: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setFilters(filters: Partial<FiltersInterface>) {
|
||||
this.filters = { ...this.filters, ...filters }
|
||||
setCookie('repod.filters', JSON.stringify(this.filters), 365)
|
||||
},
|
||||
},
|
||||
})
|
@ -1,28 +0,0 @@
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export const subscriptions = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
subscriptions: [],
|
||||
},
|
||||
mutations: {
|
||||
set: (state, subscriptions) => {
|
||||
state.subscriptions = subscriptions
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
fetch: async (context) => {
|
||||
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),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
59
src/store/subscriptions.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type {
|
||||
PersonalSettingsMetricsInterface,
|
||||
PodcastDataInterface,
|
||||
SubscriptionInterface,
|
||||
} from '../utils/types.ts'
|
||||
import { getCookie, setCookie } from '../utils/cookies.ts'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export const useSubscriptions = defineStore('subscriptions', {
|
||||
state: () => ({
|
||||
subs: [] as SubscriptionInterface[],
|
||||
}),
|
||||
getters: {
|
||||
getSubByUrl: (state) => (url: string) =>
|
||||
state.subs.find((sub) => sub.metrics.url === url),
|
||||
},
|
||||
actions: {
|
||||
async fetch() {
|
||||
let favorites: string[] = []
|
||||
try {
|
||||
favorites = JSON.parse(getCookie('repod.favorites') || '[]') || []
|
||||
} catch {}
|
||||
|
||||
const metrics = await axios.get<PersonalSettingsMetricsInterface>(
|
||||
generateUrl('/apps/gpoddersync/personal_settings/metrics'),
|
||||
)
|
||||
|
||||
this.subs = [...metrics.data.subscriptions]
|
||||
.sort((a, b) => b.listenedSeconds - a.listenedSeconds)
|
||||
.map((sub) => ({
|
||||
metrics: sub,
|
||||
isFavorite: favorites.includes(sub.url),
|
||||
data: this.subs.find((s) => s.metrics.url === sub.url)?.data,
|
||||
}))
|
||||
},
|
||||
addMetadatas(link: string, data: PodcastDataInterface) {
|
||||
this.subs = this.subs.map((sub) =>
|
||||
sub.metrics.url === link ? { ...sub, data } : sub,
|
||||
)
|
||||
},
|
||||
setFavorite(link: string, isFavorite: boolean) {
|
||||
this.subs = this.subs.map((sub) =>
|
||||
sub.metrics.url === link ? { ...sub, isFavorite } : sub,
|
||||
)
|
||||
|
||||
setCookie(
|
||||
'repod.favorites',
|
||||
JSON.stringify(
|
||||
this.subs
|
||||
.filter((sub) => sub.isFavorite)
|
||||
.map((sub) => sub.metrics.url),
|
||||
),
|
||||
365,
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
@ -4,7 +4,7 @@
|
||||
* @param {string} name Nom du cookie à récupérer
|
||||
* @return {string|null}
|
||||
*/
|
||||
export const getCookie = (name) => {
|
||||
export const getCookie = (name: string): string | null => {
|
||||
const cookies = document.cookie.split('; ')
|
||||
const value = cookies.find((c) => c.startsWith(name + '='))?.split('=')[1]
|
||||
if (value === undefined) {
|
||||
@ -19,7 +19,7 @@ export const getCookie = (name) => {
|
||||
* @param {string} value Value du cookie
|
||||
* @param {number} days Durée de vie du cookie (en jours)
|
||||
*/
|
||||
export const setCookie = (name, value, days) => {
|
||||
export const setCookie = (name: string, value: string, days: number) => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + days)
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${date.toUTCString()}; SameSite=Strict;`
|
@ -1,12 +0,0 @@
|
||||
// https://stackoverflow.com/a/53486112
|
||||
export const debounce = (fn, delay) => {
|
||||
let timeoutID = null
|
||||
return function () {
|
||||
clearTimeout(timeoutID)
|
||||
const args = arguments
|
||||
const that = this
|
||||
timeoutID = setTimeout(function () {
|
||||
fn.apply(that, args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
// https://stackoverflow.com/a/20732091
|
||||
export const humanFileSize = (size) => {
|
||||
export const humanFileSize = (size: number) => {
|
||||
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024))
|
||||
return (
|
||||
(size / Math.pow(1024, i)).toFixed(2) * 1 +
|
||||
(size / Math.pow(1024, i)).toFixed(2) +
|
||||
' ' +
|
||||
['B', 'kB', 'MB', 'GB', 'TB'][i]
|
||||
)
|
16
src/utils/status.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { EpisodeInterface } from './types'
|
||||
|
||||
export const hasEnded = (episode: EpisodeInterface) =>
|
||||
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: EpisodeInterface) =>
|
||||
episode.action &&
|
||||
episode.action.action &&
|
||||
episode.action.action.toLowerCase() === 'play' &&
|
||||
episode.action.position > 0 &&
|
||||
!hasEnded(episode)
|
@ -1,7 +0,0 @@
|
||||
// https://stackoverflow.com/a/5002618
|
||||
export const cleanHtml = (text) => {
|
||||
const pre = document.createElement('pre')
|
||||
pre.innerHTML = text.replace(/<br\s*\/?>/gm, '\n')
|
||||
const strippedText = pre.textContent || pre.innerText || ''
|
||||
return strippedText.replace(/\n/gm, '<br>')
|
||||
}
|
@ -3,9 +3,9 @@
|
||||
* @param {Date} date The date
|
||||
* @return {string}
|
||||
*/
|
||||
export const formatTimer = (date) => {
|
||||
const minutes = date.getUTCMinutes().toString().padStart(2, 0)
|
||||
const seconds = date.getUTCSeconds().toString().padStart(2, 0)
|
||||
export const formatTimer = (date: Date): string => {
|
||||
const minutes = date.getUTCMinutes().toString().padStart(2, '0')
|
||||
const seconds = date.getUTCSeconds().toString().padStart(2, '0')
|
||||
let timer = `${minutes}:${seconds}`
|
||||
|
||||
if (date.getUTCHours()) {
|
||||
@ -20,7 +20,7 @@ export const formatTimer = (date) => {
|
||||
* @param {Date} date The date
|
||||
* @return {string}
|
||||
*/
|
||||
export const formatEpisodeTimestamp = (date) => {
|
||||
export const formatEpisodeTimestamp = (date: Date): string => {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
@ -36,7 +36,7 @@ export const formatEpisodeTimestamp = (date) => {
|
||||
* @param {Date} date The date
|
||||
* @return {string}
|
||||
*/
|
||||
export const formatLocaleDate = (date) =>
|
||||
export const formatLocaleDate = (date: Date): string =>
|
||||
date.toLocaleDateString(undefined, { dateStyle: 'medium' })
|
||||
|
||||
/**
|
||||
@ -44,7 +44,7 @@ export const formatLocaleDate = (date) =>
|
||||
* @param {string} duration The duration feed's entry
|
||||
* @return {number}
|
||||
*/
|
||||
export const durationToSeconds = (duration) => {
|
||||
export const durationToSeconds = (duration: string): number => {
|
||||
const splitDuration = duration.split(':').reverse()
|
||||
let seconds = parseInt(splitDuration[0])
|
||||
seconds += splitDuration.length > 1 ? parseInt(splitDuration[1]) * 60 : 0
|
14
src/utils/toast.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import toastify from 'toastify-js'
|
||||
|
||||
export const showMessage = (text: string, backgroundColor: string) =>
|
||||
toastify({
|
||||
text,
|
||||
backgroundColor,
|
||||
}).showToast()
|
||||
|
||||
export const showError = (text: string) => showMessage(text, 'var(--color-error)')
|
||||
export const showWarning = (text: string) =>
|
||||
showMessage(text, 'var(--color-warning)')
|
||||
export const showInfo = (text: string) => showMessage(text, 'var(--color-primary)')
|
||||
export const showSuccess = (text: string) =>
|
||||
showMessage(text, 'var(--color-success)')
|
73
src/utils/types.ts
Normal file
@ -0,0 +1,73 @@
|
||||
export interface EpisodeActionInterface {
|
||||
podcast: string
|
||||
episode: string
|
||||
action: string
|
||||
timestamp: string
|
||||
started: number
|
||||
position: number
|
||||
total: number
|
||||
guid?: string
|
||||
id?: number
|
||||
}
|
||||
|
||||
export interface EpisodeInterface {
|
||||
title: string
|
||||
url: string
|
||||
name: string
|
||||
link?: string
|
||||
image?: string
|
||||
description?: string
|
||||
fetchedAtUnix: number
|
||||
guid: string
|
||||
type?: string
|
||||
size?: number
|
||||
pubDate?: {
|
||||
date: string
|
||||
timezone_type: number
|
||||
timezone: string
|
||||
}
|
||||
duration?: string
|
||||
action?: EpisodeActionInterface
|
||||
}
|
||||
|
||||
export interface FiltersInterface {
|
||||
listened: boolean
|
||||
listening: boolean
|
||||
unlistened: boolean
|
||||
}
|
||||
|
||||
export interface PodcastDataInterface {
|
||||
title: string
|
||||
author?: string
|
||||
link: string
|
||||
description?: string
|
||||
imageUrl?: string
|
||||
fetchedAtUnix: number
|
||||
imageBlob?: string | null
|
||||
}
|
||||
|
||||
export interface PodcastMetricsInterface {
|
||||
url: string
|
||||
listenedSeconds: number
|
||||
actionCounts: {
|
||||
delete: number
|
||||
download: number
|
||||
flattr: number
|
||||
new: number
|
||||
play: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface SubscriptionInterface {
|
||||
data?: PodcastDataInterface
|
||||
isFavorite: boolean
|
||||
metrics: PodcastMetricsInterface
|
||||
}
|
||||
|
||||
export interface PersonalSettingsMetricsInterface {
|
||||
subscriptions: PodcastMetricsInterface[]
|
||||
}
|
||||
|
||||
export interface PersonalSettingsPodcastDataInterface {
|
||||
data: PodcastDataInterface
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export const encodeUrl = (url) => encodeURIComponent(btoa(url))
|
||||
export const decodeUrl = (url) => atob(decodeURIComponent(url))
|
||||
export const toUrl = (url) => `/${encodeUrl(url)}`
|
5
src/utils/url.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const encodeUrl = (url: string) => encodeURIComponent(btoa(url))
|
||||
export const decodeUrl = (url: string) => atob(decodeURIComponent(url))
|
||||
export const toFeedUrl = (url: string) => `/feed/${encodeUrl(url)}`
|
||||
export const filenameFromUrl = (url: string) =>
|
||||
new URL(url).pathname.split('/').pop()
|
@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<AppContent class="main">
|
||||
<NcTextField :label="t('repod', 'Find a podcast')" :value.sync="search">
|
||||
<AppContent class="padding">
|
||||
<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" />
|
||||
@ -10,13 +12,14 @@
|
||||
</AppContent>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import AddRss from '../components/Discover/AddRss.vue'
|
||||
import AppContent from '../components/Atoms/AppContent.vue'
|
||||
import Magnify from 'vue-material-design-icons/Magnify.vue'
|
||||
import { NcTextField } from '@nextcloud/vue'
|
||||
import Search from '../components/Discover/Search.vue'
|
||||
import Toplist from '../components/Discover/Toplist.vue'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'Discover',
|
||||
@ -28,16 +31,17 @@ export default {
|
||||
Search,
|
||||
Toplist,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: () => ({
|
||||
search: '',
|
||||
}
|
||||
}),
|
||||
methods: {
|
||||
t,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
.padding {
|
||||
padding: 15px 51px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,35 +1,28 @@
|
||||
<template>
|
||||
<AppContent>
|
||||
<Loading v-if="loading" />
|
||||
<NcEmptyContent
|
||||
v-if="failed"
|
||||
class="error"
|
||||
:name="t('repod', 'Error loading feed')">
|
||||
<EmptyContent v-if="failed" :name="t('repod', 'Error loading feed')">
|
||||
<template #icon>
|
||||
<Alert />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<Banner
|
||||
v-if="feed"
|
||||
:author="feed.author"
|
||||
:description="feed.description"
|
||||
:image-url="feed.imageUrl"
|
||||
:link="feed.link"
|
||||
:title="feed.title" />
|
||||
</EmptyContent>
|
||||
<Banner v-if="feed" :feed="feed" />
|
||||
<Episodes v-if="feed" />
|
||||
</AppContent>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Alert from 'vue-material-design-icons/Alert.vue'
|
||||
import AppContent from '../components/Atoms/AppContent.vue'
|
||||
import Banner from '../components/Feed/Banner.vue'
|
||||
import EmptyContent from '../components/Atoms/EmptyContent.vue'
|
||||
import Episodes from '../components/Feed/Episodes.vue'
|
||||
import Loading from '../components/Atoms/Loading.vue'
|
||||
import { NcEmptyContent } from '@nextcloud/vue'
|
||||
import type { PodcastDataInterface } from '../utils/types.ts'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { decodeUrl } from '../utils/url.js'
|
||||
import { decodeUrl } from '../utils/url.ts'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'Feed',
|
||||
@ -37,25 +30,24 @@ export default {
|
||||
Alert,
|
||||
AppContent,
|
||||
Banner,
|
||||
EmptyContent,
|
||||
Episodes,
|
||||
Loading,
|
||||
NcEmptyContent,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: () => ({
|
||||
failed: false,
|
||||
loading: true,
|
||||
feed: null,
|
||||
}
|
||||
},
|
||||
feed: null as PodcastDataInterface | null,
|
||||
}),
|
||||
computed: {
|
||||
url() {
|
||||
return decodeUrl(this.$route.params.url)
|
||||
return decodeUrl(this.$route.params.url as string)
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
const podcastData = await axios.get(
|
||||
this.loading = true
|
||||
const podcastData = await axios.get<PodcastDataInterface>(
|
||||
generateUrl('/apps/repod/podcast?url={url}', { url: this.url }),
|
||||
)
|
||||
this.feed = podcastData.data
|
||||
@ -66,11 +58,8 @@ export default {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error {
|
||||
margin: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<NcAppContent class="content">
|
||||
<NcEmptyContent :name="t('repod', 'Missing required app')">
|
||||
<AppContent>
|
||||
<EmptyContent :name="t('repod', 'Missing required app')">
|
||||
<template #action>
|
||||
<NcButton :href="gPodderSyncUrl">
|
||||
{{ t('repod', 'Install GPodder Sync') }}
|
||||
@ -9,27 +9,33 @@
|
||||
<template #icon>
|
||||
<Alert />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
</NcAppContent>
|
||||
</EmptyContent>
|
||||
</AppContent>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NcAppContent, NcButton, NcEmptyContent } from '@nextcloud/vue'
|
||||
<script lang="ts">
|
||||
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 { t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'GPodder',
|
||||
components: {
|
||||
Alert,
|
||||
NcAppContent,
|
||||
AppContent,
|
||||
EmptyContent,
|
||||
NcButton,
|
||||
NcEmptyContent,
|
||||
},
|
||||
computed: {
|
||||
gPodderSyncUrl() {
|
||||
return generateUrl('/settings/apps/installed/gpoddersync')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
t,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
48
src/views/Home.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<AppContent>
|
||||
<EmptyContent
|
||||
v-if="!favorites.length"
|
||||
:description="
|
||||
t('repod', 'Pin some subscriptions to see their latest updates')
|
||||
"
|
||||
:name="t('repod', 'No favorites')">
|
||||
<template #icon>
|
||||
<StarOffIcon />
|
||||
</template>
|
||||
</EmptyContent>
|
||||
<ul v-if="favorites.length">
|
||||
<li v-for="favorite in favorites" :key="favorite.metrics.url">
|
||||
<Favorite :feed="favorite" />
|
||||
</li>
|
||||
</ul>
|
||||
</AppContent>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import AppContent from '../components/Atoms/AppContent.vue'
|
||||
import EmptyContent from '../components/Atoms/EmptyContent.vue'
|
||||
import Favorite from '../components/Feed/Favorite.vue'
|
||||
import StarOffIcon from 'vue-material-design-icons/StarOff.vue'
|
||||
import { mapState } from 'pinia'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { useSubscriptions } from '../store/subscriptions.ts'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
AppContent,
|
||||
EmptyContent,
|
||||
Favorite,
|
||||
StarOffIcon,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useSubscriptions, ['subs']),
|
||||
favorites() {
|
||||
return this.subs.filter((sub) => sub.isFavorite)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
t,
|
||||
},
|
||||
}
|
||||
</script>
|
@ -6,7 +6,9 @@ namespace OCA\GPodderSync\Core\SubscriptionChange;
|
||||
|
||||
class SubscriptionChangeRequestParser
|
||||
{
|
||||
public function __construct(private SubscriptionChangesReader $subscriptionChangeReader) {}
|
||||
public function __construct(
|
||||
private SubscriptionChangesReader $subscriptionChangeReader
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return SubscriptionChange[]
|
||||
|
@ -8,7 +8,9 @@ use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction;
|
||||
|
||||
class EpisodeActionRepository
|
||||
{
|
||||
public function __construct(private EpisodeActionMapper $episodeActionMapper) {}
|
||||
public function __construct(
|
||||
private EpisodeActionMapper $episodeActionMapper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return EpisodeAction[]
|
||||
|
@ -8,7 +8,9 @@ use OCP\DB\Exception;
|
||||
|
||||
class EpisodeActionWriter
|
||||
{
|
||||
public function __construct(private EpisodeActionMapper $episodeActionMapper) {}
|
||||
public function __construct(
|
||||
private EpisodeActionMapper $episodeActionMapper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return EpisodeActionEntity
|
||||
|
@ -6,7 +6,9 @@ namespace OCA\GPodderSync\Db\SubscriptionChange;
|
||||
|
||||
class SubscriptionChangeRepository
|
||||
{
|
||||
public function __construct(private SubscriptionChangeMapper $subscriptionChangeMapper) {}
|
||||
public function __construct(
|
||||
private SubscriptionChangeMapper $subscriptionChangeMapper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return SubscriptionChangeEntity[]
|
||||
|
@ -6,7 +6,9 @@ namespace OCA\GPodderSync\Db\SubscriptionChange;
|
||||
|
||||
class SubscriptionChangeWriter
|
||||
{
|
||||
public function __construct(private SubscriptionChangeMapper $subscriptionChangeMapper) {}
|
||||
public function __construct(
|
||||
private SubscriptionChangeMapper $subscriptionChangeMapper
|
||||
) {}
|
||||
|
||||
public function purge(): void {}
|
||||
|
||||
|
170
translationfiles/de/repod.po
Normal file
@ -0,0 +1,170 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the Nextcloud package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Nextcloud 3.14159\n"
|
||||
"Report-Msgid-Bugs-To: translations\\@example.com\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: OiledAmoeba <florian+crystalyx@ruhnke.cloud>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "RePod Subscriptions"
|
||||
msgstr "RePod Abonnements"
|
||||
|
||||
msgid "Podcast"
|
||||
msgstr "Podcast"
|
||||
|
||||
msgid "RePod"
|
||||
msgstr "RePod"
|
||||
|
||||
msgid "🔊 Browse, manage and listen to podcasts"
|
||||
msgstr "🔊 Suchen, Verwalten und Anhören von Podcasts"
|
||||
|
||||
msgid ""
|
||||
"## Features\n"
|
||||
"- 🔍 Browse and subscribe huge collection of podcasts\n"
|
||||
"- 🔊 Listen to episodes directly in Nextcloud\n"
|
||||
"- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/) and [other "
|
||||
"apps](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-"
|
||||
"gpoddersync)\n"
|
||||
"- 📱 Mobile friendly interface\n"
|
||||
"- 📡 Import and export your subscriptions\n"
|
||||
"- ➡️ Full features comparison [here](https://git.crystalyx.net/Xefir/"
|
||||
"repod#comparaison-with-similar-apps-for-nextcloud)\n"
|
||||
"\n"
|
||||
"## Requirements\n"
|
||||
"You need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) "
|
||||
"installed to use this app!"
|
||||
msgstr ""
|
||||
"## Funktionen\n"
|
||||
"- 🔍 Durchsuchen und abonnieren einer großen Sammlung von Podcasts\n"
|
||||
"- 🔊 Episoden direkt in Nextcloud anhören\n"
|
||||
"- 🌐 Synchronisiere deine Aktivität mit [AntennaPod](https://antennapod.org/)\n"
|
||||
"- 📱 Mobile-freundliche Schnittstelle\n"
|
||||
"- 📡 Importieren und Exportieren Ihrer Abonnements\n"
|
||||
"\n"
|
||||
"## Voraussetzungen\n"
|
||||
"Du musst [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) "
|
||||
"installiert haben, um diese App zu benutzen!"
|
||||
|
||||
msgid "Download"
|
||||
msgstr "Herunterladen"
|
||||
|
||||
msgid "Add a RSS link"
|
||||
msgstr "Einen RSS-Link hinzufügen"
|
||||
|
||||
msgid "Subscribe"
|
||||
msgstr "Abonnieren"
|
||||
|
||||
msgid "Error while adding the feed"
|
||||
msgstr "Fehler beim Hinzufügen des Feeds"
|
||||
|
||||
msgid "Could not fetch search results"
|
||||
msgstr "Suchergebnisse können nicht geladen werden"
|
||||
|
||||
msgid "New podcasts"
|
||||
msgstr "Neue Podcasts"
|
||||
|
||||
msgid "Hot podcasts"
|
||||
msgstr "Beliebte Podcasts"
|
||||
|
||||
msgid "Could not fetch tops"
|
||||
msgstr "Titel können nicht abgerufen werden"
|
||||
|
||||
msgid "Copy feed"
|
||||
msgstr "Feed kopieren"
|
||||
|
||||
msgid "Link copied to the clipboard"
|
||||
msgstr "Der Link des Feeds wurde in die Zwischenablage kopiert"
|
||||
|
||||
msgid "Play"
|
||||
msgstr "Abspielen"
|
||||
|
||||
msgid "Stop"
|
||||
msgstr "Stopp"
|
||||
|
||||
msgid "Read"
|
||||
msgstr "Gelesen"
|
||||
|
||||
msgid "Open website"
|
||||
msgstr "Webseite aufrufen"
|
||||
|
||||
msgid "Could not change the status of the episode"
|
||||
msgstr "Kann den Status der Folge nicht ändern"
|
||||
|
||||
msgid "Could not fetch episodes"
|
||||
msgstr "Folgen können nicht abgerufen werden"
|
||||
|
||||
msgid "Export subscriptions"
|
||||
msgstr "Abonnements exportieren"
|
||||
|
||||
msgid "Filtering episodes"
|
||||
msgstr "Folgen filtern"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr "Zeige alles"
|
||||
|
||||
msgid "Listened"
|
||||
msgstr "Gehört"
|
||||
|
||||
msgid "Listening"
|
||||
msgstr "Läuft"
|
||||
|
||||
msgid "Unlistened"
|
||||
msgstr "Nicht angehört"
|
||||
|
||||
msgid "Import subscriptions"
|
||||
msgstr "Importiere Abonnements"
|
||||
|
||||
msgid "Import OPML file"
|
||||
msgstr "Importiere OPML-Datei"
|
||||
|
||||
msgid "Rate RePod ❤️"
|
||||
msgstr "Bewerte RePod ❤️"
|
||||
|
||||
msgid "Playback speed"
|
||||
msgstr "Wiedergabegeschwindigkeit"
|
||||
|
||||
msgid "Favorite"
|
||||
msgstr "Favorit"
|
||||
|
||||
msgid "Are you sure you want to delete this subscription?"
|
||||
msgstr "Bist Du sicher, dass Du das Abonnement löschen möchtest?"
|
||||
|
||||
msgid "Error while removing the feed"
|
||||
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"
|
||||
msgstr "Einen Podcast hinzufügen"
|
||||
|
||||
msgid "Could not fetch subscriptions"
|
||||
msgstr "Abonnements können nicht abgerufen werden"
|
||||
|
||||
msgid "Find a podcast"
|
||||
msgstr "Finde einen Podcast"
|
||||
|
||||
msgid "Error loading feed"
|
||||
msgstr "Fehler beim Laden des Feeds"
|
||||
|
||||
msgid "Missing required app"
|
||||
msgstr "Benötigte App fehlt"
|
||||
|
||||
msgid "Install 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"
|
@ -9,9 +9,9 @@ msgstr ""
|
||||
"Project-Id-Version: Nextcloud 3.14159\n"
|
||||
"Report-Msgid-Bugs-To: translations\\@example.com\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Last-Translator: Michel Roux <xefir@crystalyx.net>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
@ -32,7 +32,13 @@ msgid ""
|
||||
"## Features\n"
|
||||
"- 🔍 Browse and subscribe huge collection of podcasts\n"
|
||||
"- 🔊 Listen to episodes directly in Nextcloud\n"
|
||||
"- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/)\n"
|
||||
"- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/) and [other "
|
||||
"apps](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-"
|
||||
"gpoddersync)\n"
|
||||
"- 📱 Mobile friendly interface\n"
|
||||
"- 📡 Import and export your subscriptions\n"
|
||||
"- ➡️ Full features comparison [here](https://git.crystalyx.net/Xefir/"
|
||||
"repod#comparaison-with-similar-apps-for-nextcloud)\n"
|
||||
"\n"
|
||||
"## Requirements\n"
|
||||
"You need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) "
|
||||
@ -41,7 +47,13 @@ msgstr ""
|
||||
"## Fonctionnalités\n"
|
||||
"- 🔍 Parcourir et s'abonner à une grande collections de podcasts\n"
|
||||
"- 🔊 Écouter vos épisodes directement sur Nextcloud\n"
|
||||
"- 🌐 Synchroniser son activité avec [AntennaPod](https://antennapod.org/)\n"
|
||||
"- 🌐 Synchroniser son activité avec [AntennaPod](https://antennapod.org/) et d'autres "
|
||||
"[applications](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-"
|
||||
"gpoddersync)\n"
|
||||
"- 📱 Interface optimisée pour mobiles et ordinateurs\n"
|
||||
"- 📡 Import/export de ses abonnements\n"
|
||||
"- ➡️ Tableau récapitulatif complet des fonctionnalitées [ici](https://git.crystalyx."
|
||||
"net/Xefir/repod#comparaison-with-similar-apps-for-nextcloud)\n"
|
||||
"\n"
|
||||
"## Pré-requis\n"
|
||||
"Vous devez avoir [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) "
|
||||
@ -74,27 +86,27 @@ msgstr "Impossible de récupérer les tops"
|
||||
msgid "Copy feed"
|
||||
msgstr "Copier le flux"
|
||||
|
||||
msgid "Link copied to the clipboard"
|
||||
msgstr "Lien vers le flux copié dans le presse-papiers"
|
||||
|
||||
msgid "Play"
|
||||
msgstr "Lecture"
|
||||
|
||||
msgid "Stop"
|
||||
msgstr "Arrêter"
|
||||
|
||||
msgid "Mark as read"
|
||||
msgstr "Marquer comme lu"
|
||||
|
||||
msgid "Mark as unread"
|
||||
msgstr "Marquer comme non lu"
|
||||
msgid "Read"
|
||||
msgstr "Lu"
|
||||
|
||||
msgid "Open website"
|
||||
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"
|
||||
msgstr "Impossible de changer le status de l'épisode"
|
||||
|
||||
msgid "Could not fetch episodes"
|
||||
msgstr "Impossible de récuprer les épisodes"
|
||||
|
||||
msgid "Export subscriptions"
|
||||
msgstr "Exporter les abonnements"
|
||||
|
||||
@ -125,12 +137,18 @@ msgstr "Donnez votre avis ❤️"
|
||||
msgid "Playback speed"
|
||||
msgstr "Vitesse de lecture"
|
||||
|
||||
msgid "Favorite"
|
||||
msgstr "Favori"
|
||||
|
||||
msgid "Are you sure you want to delete this subscription?"
|
||||
msgstr "Êtes-vous sûr de vouloir supprimer ce flux ?"
|
||||
|
||||
msgid "Error while removing the feed"
|
||||
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"
|
||||
msgstr "Ajouter un podcast"
|
||||
|
||||
@ -149,5 +167,8 @@ msgstr "Une application requise est manquante"
|
||||
msgid "Install GPodder Sync"
|
||||
msgstr "Installer GPodder Sync"
|
||||
|
||||
msgid "Feed's link copied to the clipboard"
|
||||
msgstr "Lien vers le flux copié dans le presse-papiers"
|
||||
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"
|
||||
|
@ -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-05-29 14:56+0000\n"
|
||||
"POT-Creation-Date: 2024-09-15 13:40+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"
|
||||
@ -40,7 +40,13 @@ msgid ""
|
||||
"## Features\n"
|
||||
"- 🔍 Browse and subscribe huge collection of podcasts\n"
|
||||
"- 🔊 Listen to episodes directly in Nextcloud\n"
|
||||
"- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/)\n"
|
||||
"- 🌐 Sync your activity with [AntennaPod](https://antennapod.org/) and [other "
|
||||
"apps](https://git.crystalyx.net/Xefir/repod#clients-supporting-sync-of-"
|
||||
"gpoddersync)\n"
|
||||
"- 📱 Mobile friendly interface\n"
|
||||
"- 📡 Import and export your subscriptions\n"
|
||||
"- ➡️ Full features comparison [here](https://git.crystalyx.net/Xefir/"
|
||||
"repod#comparaison-with-similar-apps-for-nextcloud)\n"
|
||||
"\n"
|
||||
"## Requirements\n"
|
||||
"You need to have [GPodderSync](https://apps.nextcloud.com/apps/gpoddersync) "
|
||||
@ -48,8 +54,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:1
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:27
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:28
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:24
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:25
|
||||
msgid "Download"
|
||||
msgstr ""
|
||||
|
||||
@ -59,42 +65,45 @@ msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:3
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:4
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:11
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:5
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:12
|
||||
msgid "Subscribe"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:5
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:12
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:6
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:13
|
||||
msgid "Error while adding the feed"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:6
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:7
|
||||
msgid "Could not fetch search results"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:7
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:8
|
||||
msgid "New podcasts"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:8
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:9
|
||||
msgid "Hot podcasts"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:9
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:10
|
||||
msgid "Could not fetch tops"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:10
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:11
|
||||
msgid "Copy feed"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:13
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:14
|
||||
msgid "Link copied to the clipboard"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:15
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:16
|
||||
msgid "Play"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:16
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:17
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:18
|
||||
msgid "Stop"
|
||||
@ -103,96 +112,109 @@ msgstr ""
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:19
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:20
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:21
|
||||
msgid "Mark as read"
|
||||
msgid "Read"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:22
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:23
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:24
|
||||
msgid "Mark as unread"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:25
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:26
|
||||
msgid "Open website"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:29
|
||||
msgid "Could not fetch episodes"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:30
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:26
|
||||
msgid "Could not change the status of the episode"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:32
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:30
|
||||
msgid "Filtering episodes"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:33
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:31
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:34
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:32
|
||||
msgid "Listened"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:35
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:33
|
||||
msgid "Listening"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:36
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:34
|
||||
msgid "Unlistened"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:37
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:35
|
||||
msgid "Import subscriptions"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:38
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:36
|
||||
msgid "Import OPML file"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:39
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:37
|
||||
msgid "Rate RePod ❤️"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:40
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:38
|
||||
msgid "Playback speed"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:39
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:40
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:41
|
||||
msgid "Are you sure you want to delete this subscription?"
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:42
|
||||
msgid "Error while removing the feed"
|
||||
msgid "Are you sure you want to delete this subscription?"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:43
|
||||
msgid "Add a podcast"
|
||||
msgid "Error while removing the feed"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:44
|
||||
msgid "Could not fetch subscriptions"
|
||||
msgid "You can only have 10 favorites"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:45
|
||||
msgid "Find a podcast"
|
||||
msgid "Add a podcast"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:46
|
||||
msgid "Error loading feed"
|
||||
msgid "Could not fetch subscriptions"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:47
|
||||
msgid "Missing required app"
|
||||
msgid "Find a podcast"
|
||||
msgstr ""
|
||||
|
||||
#: /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"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:51
|
||||
msgid "Pin some subscriptions to see their latest updates"
|
||||
msgstr ""
|
||||
|
||||
#: /app/specialVueFakeDummyForL10nScript.js:52
|
||||
msgid "No favorites"
|
||||
msgstr ""
|
||||
|
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.vue", "**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noImplicitAny": false,
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"target": 3.3,
|
||||
},
|
||||
}
|
25
vite.config.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { join, resolve } from 'path'
|
||||
import { createAppConfig } from '@nextcloud/vite-config'
|
||||
import { defineConfig } from 'vite'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
const config = defineConfig(({ mode }) => ({
|
||||
build: {
|
||||
sourcemap: false,
|
||||
},
|
||||
define: {
|
||||
__VUE_PROD_DEVTOOLS__: mode !== 'production',
|
||||
},
|
||||
plugins: [vueDevTools()],
|
||||
}))
|
||||
|
||||
export default createAppConfig(
|
||||
{
|
||||
main: resolve(join('src', 'main.ts')),
|
||||
},
|
||||
{
|
||||
config,
|
||||
createEmptyCSSEntryPoints: true,
|
||||
thirdPartyLicense: false,
|
||||
},
|
||||
)
|
31
webpack.js
@ -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
|