Compare commits

..

No commits in common. "29c29cdfdda8a93e4aa9cc2ac34eedbfcd47db9d" and "eae106e72bd4f5bb18e481e8651a52abc0860c0f" have entirely different histories.

57 changed files with 594 additions and 926 deletions

View File

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

View File

@ -15,7 +15,7 @@
"psalm": "psalm --threads=1 --no-cache --show-info=true" "psalm": "psalm --threads=1 --no-cache --show-info=true"
}, },
"require-dev": { "require-dev": {
"nextcloud/ocp": "^30.0.0", "nextcloud/ocp": "^29.0.6",
"roave/security-advisories": "dev-latest", "roave/security-advisories": "dev-latest",
"nextcloud/coding-standard": "^1.2.3", "nextcloud/coding-standard": "^1.2.3",
"vimeo/psalm": "^5.26.1" "vimeo/psalm": "^5.26.1"

26
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "f21708cb7d3f7f4033053536e295e633", "content-hash": "d1d678bc8d001322808264320c120173",
"packages": [], "packages": [],
"packages-dev": [ "packages-dev": [
{ {
@ -733,16 +733,16 @@
}, },
{ {
"name": "nextcloud/ocp", "name": "nextcloud/ocp",
"version": "v30.0.0", "version": "v29.0.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nextcloud-deps/ocp.git", "url": "https://github.com/nextcloud-deps/ocp.git",
"reference": "a26b4e1f75983f359bd835c2529ce37b5599d58f" "reference": "b130f11ce24351a6a91115aa6f386271f7aeee9d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/a26b4e1f75983f359bd835c2529ce37b5599d58f", "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/b130f11ce24351a6a91115aa6f386271f7aeee9d",
"reference": "a26b4e1f75983f359bd835c2529ce37b5599d58f", "reference": "b130f11ce24351a6a91115aa6f386271f7aeee9d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -755,7 +755,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-stable30": "30.0.0-dev" "dev-stable29": "29.0.0-dev"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
@ -771,9 +771,9 @@
"description": "Composer package containing Nextcloud's public API (classes, interfaces)", "description": "Composer package containing Nextcloud's public API (classes, interfaces)",
"support": { "support": {
"issues": "https://github.com/nextcloud-deps/ocp/issues", "issues": "https://github.com/nextcloud-deps/ocp/issues",
"source": "https://github.com/nextcloud-deps/ocp/tree/v30.0.0" "source": "https://github.com/nextcloud-deps/ocp/tree/v29.0.7"
}, },
"time": "2024-09-13T00:40:45+00:00" "time": "2024-09-05T00:40:09+00:00"
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
@ -1312,12 +1312,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git", "url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "fb263701a24214c3176ef23bfa98a7cbc59aa659" "reference": "ed0688c3e18bf76d2a17fb243b99acb52c2e29ef"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/fb263701a24214c3176ef23bfa98a7cbc59aa659", "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/ed0688c3e18bf76d2a17fb243b99acb52c2e29ef",
"reference": "fb263701a24214c3176ef23bfa98a7cbc59aa659", "reference": "ed0688c3e18bf76d2a17fb243b99acb52c2e29ef",
"shasum": "" "shasum": ""
}, },
"conflict": { "conflict": {
@ -1769,7 +1769,7 @@
"phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5", "phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5",
"phpoffice/common": "<0.2.9", "phpoffice/common": "<0.2.9",
"phpoffice/phpexcel": "<1.8", "phpoffice/phpexcel": "<1.8",
"phpoffice/phpspreadsheet": "<1.29.1|>=2,<2.1.1|>=2.2,<2.2.1", "phpoffice/phpspreadsheet": "<1.29.1|>=2,<2.2.1",
"phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36",
"phpservermon/phpservermon": "<3.6", "phpservermon/phpservermon": "<3.6",
"phpsysinfo/phpsysinfo": "<3.4.3", "phpsysinfo/phpsysinfo": "<3.4.3",
@ -2126,7 +2126,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-13T14:04:35+00:00" "time": "2024-09-10T18:06:22+00:00"
}, },
{ {
"name": "sebastian/diff", "name": "sebastian/diff",

View File

@ -26,7 +26,7 @@ class PageController extends Controller
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function index(): TemplateResponse { public function index(): TemplateResponse {
Util::addScript(Application::APP_ID, Application::APP_ID.'-main'); Util::addScript(Application::APP_ID, 'main');
$csp = new ContentSecurityPolicy(); $csp = new ContentSecurityPolicy();
$csp->addAllowedImageDomain('*'); $csp->addAllowedImageDomain('*');

637
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,8 @@
{ {
"name": "repod", "name": "repod",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"type": "module",
"scripts": { "scripts": {
"build": "vue-tsc && vite build --mode production", "build": "vite build --mode production",
"dev": "vite build --mode development", "dev": "vite build --mode development",
"dev:watch": "vite build --mode development --watch", "dev:watch": "vite build --mode development --watch",
"watch": "npm run dev:watch", "watch": "npm run dev:watch",
@ -12,6 +11,9 @@
"stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css", "stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css",
"stylelint:fix": "stylelint src/**/*.vue src/**/*.scss src/**/*.css --fix" "stylelint:fix": "stylelint src/**/*.vue src/**/*.scss src/**/*.css --fix"
}, },
"browserslist": [
"extends @nextcloud/browserslist-config"
],
"prettier": "@nextcloud/prettier-config", "prettier": "@nextcloud/prettier-config",
"dependencies": { "dependencies": {
"@nextcloud/axios": "^2.5.0", "@nextcloud/axios": "^2.5.0",
@ -24,29 +26,19 @@
"linkify-html": "^4.1.3", "linkify-html": "^4.1.3",
"pinia": "^2.2.2", "pinia": "^2.2.2",
"toastify-js": "^1.12.0", "toastify-js": "^1.12.0",
"vite": "^5.4.5", "vite": "^5.4.3",
"vite-plugin-vue-devtools": "^7.4.5", "vite-plugin-vue-devtools": "^7.4.4",
"vue": "^3.5.5", "vue": "^3.5.4",
"vue-material-design-icons": "^5.3.0", "vue-material-design-icons": "^5.3.0",
"vue-router": "^4.4.5" "vue-router": "^4.4.4"
}, },
"devDependencies": { "devDependencies": {
"@nextcloud/browserslist-config": "^3.0.1", "@nextcloud/browserslist-config": "^3.0.1",
"@nextcloud/eslint-config": "^8.4.1", "@nextcloud/eslint-config": "^8.4.1",
"@nextcloud/prettier-config": "^1.1.0", "@nextcloud/prettier-config": "^1.1.0",
"@nextcloud/stylelint-config": "^3.0.1", "@nextcloud/stylelint-config": "^3.0.1",
"@types/toastify-js": "^1.12.3",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-pinia": "^0.4.1", "eslint-plugin-pinia": "^0.4.1",
"eslint-plugin-prettier": "^5.2.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"
]
} }

View File

@ -7,7 +7,7 @@
</NcContent> </NcContent>
</template> </template>
<script lang="ts"> <script>
import 'toastify-js/src/toastify.css' import 'toastify-js/src/toastify.css'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import Bar from './components/Player/Bar.vue' import Bar from './components/Player/Bar.vue'
@ -15,7 +15,7 @@ import GPodder from './views/GPodder.vue'
import { NcContent } from '@nextcloud/vue' import { NcContent } from '@nextcloud/vue'
import Subscriptions from './components/Sidebar/Subscriptions.vue' import Subscriptions from './components/Sidebar/Subscriptions.vue'
import { loadState } from '@nextcloud/initial-state' import { loadState } from '@nextcloud/initial-state'
import { usePlayer } from './store/player.ts' import { usePlayer } from './store/player.js'
export default { export default {
name: 'App', name: 'App',

View File

@ -4,10 +4,10 @@
</NcAppContent> </NcAppContent>
</template> </template>
<script lang="ts"> <script>
import { NcAppContent } from '@nextcloud/vue' import { NcAppContent } from '@nextcloud/vue'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.js'
export default { export default {
name: 'AppContent', name: 'AppContent',

View File

@ -10,10 +10,10 @@
</NcAppNavigation> </NcAppNavigation>
</template> </template>
<script lang="ts"> <script>
import { NcAppNavigation } from '@nextcloud/vue' import { NcAppNavigation } from '@nextcloud/vue'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.js'
export default { export default {
name: 'AppNavigation', name: 'AppNavigation',

View File

@ -16,7 +16,7 @@
</NcEmptyContent> </NcEmptyContent>
</template> </template>
<script lang="ts"> <script>
import { NcEmptyContent } from '@nextcloud/vue' import { NcEmptyContent } from '@nextcloud/vue'
export default { export default {

View File

@ -2,7 +2,7 @@
<NcLoadingIcon class="loading" /> <NcLoadingIcon class="loading" />
</template> </template>
<script lang="ts"> <script>
import { NcLoadingIcon } from '@nextcloud/vue' import { NcLoadingIcon } from '@nextcloud/vue'
export default { export default {

View File

@ -6,7 +6,7 @@
:size="256" :size="256"
:url="episode.image" /> :url="episode.image" />
<h2>{{ episode.name }}</h2> <h2>{{ episode.name }}</h2>
<SafeHtml :source="episode.description || ''" /> <SafeHtml :source="episode.description" />
<div class="flex"> <div class="flex">
<NcButton v-if="episode.link" :href="episode.link" target="_blank"> <NcButton v-if="episode.link" :href="episode.link" target="_blank">
<template #icon> <template #icon>
@ -29,15 +29,13 @@
</div> </div>
</template> </template>
<script lang="ts"> <script>
import { NcAvatar, NcButton } from '@nextcloud/vue' import { NcAvatar, NcButton } from '@nextcloud/vue'
import DownloadIcon from 'vue-material-design-icons/Download.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 OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import SafeHtml from './SafeHtml.vue' import SafeHtml from './SafeHtml.vue'
import { filenameFromUrl } from '../../utils/url.ts' import { filenameFromUrl } from '../../utils/url.js'
import { humanFileSize } from '../../utils/size.ts' import { humanFileSize } from '../../utils/size.js'
import { t } from '@nextcloud/l10n'
export default { export default {
name: 'Modal', name: 'Modal',
@ -50,14 +48,13 @@ export default {
}, },
props: { props: {
episode: { episode: {
type: Object as () => EpisodeInterface, type: Object,
required: true, required: true,
}, },
}, },
methods: { methods: {
filenameFromUrl, filenameFromUrl,
humanFileSize, humanFileSize,
t,
}, },
} }
</script> </script>

View File

@ -2,7 +2,7 @@
<div v-sanitize="source" class="html" /> <div v-sanitize="source" class="html" />
</template> </template>
<script lang="ts"> <script>
import dompurify from 'dompurify' import dompurify from 'dompurify'
import linkifyHtml from 'linkify-html' import linkifyHtml from 'linkify-html'

View File

@ -10,11 +10,10 @@
</NcAppNavigationList> </NcAppNavigationList>
</template> </template>
<script lang="ts"> <script>
import { NcAppNavigationList, NcAppNavigationNewItem } from '@nextcloud/vue' import { NcAppNavigationList, NcAppNavigationNewItem } from '@nextcloud/vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue' import PlusIcon from 'vue-material-design-icons/Plus.vue'
import { t } from '@nextcloud/l10n' import { toFeedUrl } from '../../utils/url.js'
import { toFeedUrl } from '../../utils/url.ts'
export default { export default {
name: 'AddRss', name: 'AddRss',
@ -24,7 +23,6 @@ export default {
PlusIcon, PlusIcon,
}, },
methods: { methods: {
t,
toFeedUrl, toFeedUrl,
}, },
} }

View File

@ -19,7 +19,7 @@
</template> </template>
<template #actions> <template #actions>
<NcActionButton <NcActionButton
v-if="!getSubByUrl(feed.link)" v-if="!getSubscriptions.includes(feed.link)"
:aria-label="t('repod', 'Subscribe')" :aria-label="t('repod', 'Subscribe')"
:name="t('repod', 'Subscribe')" :name="t('repod', 'Subscribe')"
:title="t('repod', 'Subscribe')" :title="t('repod', 'Subscribe')"
@ -34,19 +34,18 @@
</div> </div>
</template> </template>
<script lang="ts"> <script>
import { NcActionButton, NcAvatar, NcListItem } from '@nextcloud/vue' import { NcActionButton, NcAvatar, NcListItem } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import Loading from '../Atoms/Loading.vue' import Loading from '../Atoms/Loading.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue' import PlusIcon from 'vue-material-design-icons/Plus.vue'
import type { PodcastDataInterface } from '../../utils/types.ts'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { formatLocaleDate } from '../../utils/time.ts' import { debounce } from '../../utils/debounce.js'
import { formatLocaleDate } from '../../utils/time.js'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { showError } from '../../utils/toast.ts' import { showError } from '../../utils/toast.js'
import { t } from '@nextcloud/l10n' import { toFeedUrl } from '../../utils/url.js'
import { toFeedUrl } from '../../utils/url.ts' import { useSubscriptions } from '../../store/subscriptions.js'
import { useSubscriptions } from '../../store/subscriptions.ts'
export default { export default {
name: 'Search', name: 'Search',
@ -64,27 +63,22 @@ export default {
}, },
}, },
data: () => ({ data: () => ({
feeds: [] as PodcastDataInterface[], feeds: [],
loading: false, loading: false,
timeout: null as NodeJS.Timeout | null,
}), }),
computed: { computed: {
...mapState(useSubscriptions, ['getSubByUrl']), ...mapState(useSubscriptions, ['getSubscriptions']),
}, },
watch: { watch: {
value() { value() {
if (this.timeout) { this.search()
clearTimeout(this.timeout)
}
this.timeout = setTimeout(this.search, 200)
}, },
}, },
methods: { methods: {
...mapActions(useSubscriptions, ['fetch']), ...mapActions(useSubscriptions, ['fetch']),
formatLocaleDate, formatLocaleDate,
t,
toFeedUrl, toFeedUrl,
async addSubscription(url: string) { async addSubscription(url) {
try { try {
await axios.post( await axios.post(
generateUrl('/apps/gpoddersync/subscription_change/create'), generateUrl('/apps/gpoddersync/subscription_change/create'),
@ -97,13 +91,14 @@ export default {
console.error(e) console.error(e)
showError(t('repod', 'Error while adding the feed')) showError(t('repod', 'Error while adding the feed'))
} }
this.fetch() this.fetch()
}, },
async search() { search: debounce(async function value() {
try { try {
this.loading = true this.loading = true
const currentSearch = this.value const currentSearch = this.value
const feeds = await axios.get<PodcastDataInterface[]>( const feeds = await axios.get(
generateUrl('/apps/repod/search?q={value}', { generateUrl('/apps/repod/search?q={value}', {
value: currentSearch, value: currentSearch,
}), }),
@ -121,7 +116,7 @@ export default {
this.loading = false this.loading = false
} }
} }
}, }, 200),
}, },
} }
</script> </script>

View File

@ -12,14 +12,12 @@
</div> </div>
</template> </template>
<script lang="ts"> <script>
import Loading from '../Atoms/Loading.vue' import Loading from '../Atoms/Loading.vue'
import type { PodcastDataInterface } from '../../utils/types.ts'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { showError } from '../../utils/toast.ts' import { showError } from '../../utils/toast.js'
import { t } from '@nextcloud/l10n' import { toFeedUrl } from '../../utils/url.js'
import { toFeedUrl } from '../../utils/url.ts'
export default { export default {
name: 'Toplist', name: 'Toplist',
@ -34,7 +32,7 @@ export default {
}, },
data: () => ({ data: () => ({
loading: true, loading: true,
tops: [] as PodcastDataInterface[], tops: [],
}), }),
computed: { computed: {
title() { title() {
@ -51,8 +49,8 @@ export default {
async mounted() { async mounted() {
try { try {
this.loading = true this.loading = true
const tops = await axios.get<PodcastDataInterface[]>( const tops = await axios.get(
generateUrl('/apps/repod/toplist/{type}', { type: this.type }), generateUrl(`/apps/repod/toplist/${this.type}`),
) )
this.tops = tops.data this.tops = tops.data
} catch (e) { } catch (e) {

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="header"> <div class="header">
<img class="background" :src="feed.imageUrl" /> <img class="background" :src="imageUrl" />
<div class="content"> <div class="content">
<div> <div>
<NcAvatar <NcAvatar
:display-name="feed.author || feed.title" :display-name="author || title"
:is-no-user="true" :is-no-user="true"
:size="128" :size="128"
:url="feed.imageUrl" /> :url="imageUrl" />
<a class="feed" :href="url" @click.prevent="copyFeed"> <a class="feed" :href="url" @click.prevent="copyFeed">
<RssIcon :size="20" /> <RssIcon :size="20" />
<i>{{ t('repod', 'Copy feed') }}</i> <i>{{ t('repod', 'Copy feed') }}</i>
@ -15,15 +15,15 @@
</div> </div>
<div class="inner"> <div class="inner">
<div class="infos"> <div class="infos">
<h2>{{ feed.title }}</h2> <h2>{{ title }}</h2>
<a :href="feed.link" target="_blank"> <a :href="link" target="_blank">
<i>{{ feed.author }}</i> <i>{{ author }}</i>
</a> </a>
<br /><br /> <br /><br />
<SafeHtml :source="feed.description || ''" /> <SafeHtml :source="description" />
</div> </div>
<NcAppNavigationNew <NcAppNavigationNew
v-if="!getSubByUrl(url)" v-if="!getSubscriptions.includes(url)"
:text="t('repod', 'Subscribe')" :text="t('repod', 'Subscribe')"
@click="addSubscription"> @click="addSubscription">
<template #icon> <template #icon>
@ -35,19 +35,17 @@
</div> </div>
</template> </template>
<script lang="ts"> <script>
import { NcAppNavigationNew, NcAvatar } from '@nextcloud/vue' import { NcAppNavigationNew, NcAvatar } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import { showError, showSuccess } from '../../utils/toast.ts' import { showError, showSuccess } from '../../utils/toast.js'
import PlusIcon from 'vue-material-design-icons/Plus.vue' 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 RssIcon from 'vue-material-design-icons/Rss.vue'
import SafeHtml from '../Atoms/SafeHtml.vue' import SafeHtml from '../Atoms/SafeHtml.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { decodeUrl } from '../../utils/url.ts' import { decodeUrl } from '../../utils/url.js'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { t } from '@nextcloud/l10n' import { useSubscriptions } from '../../store/subscriptions.js'
import { useSubscriptions } from '../../store/subscriptions.ts'
export default { export default {
name: 'Banner', name: 'Banner',
@ -59,20 +57,35 @@ export default {
SafeHtml, SafeHtml,
}, },
props: { props: {
feed: { author: {
type: Object as () => PodcastDataInterface, type: String,
required: true,
},
description: {
type: String,
required: true,
},
imageUrl: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
title: {
type: String,
required: true, required: true,
}, },
}, },
computed: { computed: {
...mapState(useSubscriptions, ['getSubByUrl']), ...mapState(useSubscriptions, ['getSubscriptions']),
url() { url() {
return decodeUrl(this.$route.params.url as string) return decodeUrl(this.$route.params.url)
}, },
}, },
methods: { methods: {
...mapActions(useSubscriptions, ['fetch']), ...mapActions(useSubscriptions, ['fetch']),
t,
async addSubscription() { async addSubscription() {
try { try {
await axios.post( await axios.post(
@ -86,6 +99,7 @@ export default {
console.error(e) console.error(e)
showError(t('repod', 'Error while adding the feed')) showError(t('repod', 'Error while adding the feed'))
} }
this.fetch() this.fetch()
}, },
copyFeed() { copyFeed() {
@ -129,13 +143,11 @@ export default {
} }
.infos { .infos {
flex: 1;
overflow: auto; overflow: auto;
} }
.inner { .inner {
display: flex; display: flex;
flex: 1;
} }
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {

View File

@ -2,11 +2,7 @@
<NcListItem <NcListItem
:active="isCurrentEpisode(episode)" :active="isCurrentEpisode(episode)"
class="episode" class="episode"
:details=" :details="!oneLine ? formatLocaleDate(new Date(episode.pubDate?.date)) : ''"
!oneLine && episode.pubDate
? formatLocaleDate(new Date(episode.pubDate?.date))
: ''
"
:force-display-actions="true" :force-display-actions="true"
:name="episode.name" :name="episode.name"
:one-line="oneLine" :one-line="oneLine"
@ -82,7 +78,7 @@
</template> </template>
<template #indicator> <template #indicator>
<NcProgressBar <NcProgressBar
v-if="episode.action && isListening(episode) && !oneLine" v-if="isListening(episode) && !oneLine"
class="progress" class="progress"
:value="(episode.action.position * 100) / episode.action.total" /> :value="(episode.action.position * 100) / episode.action.total" />
</template> </template>
@ -92,7 +88,7 @@
</NcListItem> </NcListItem>
</template> </template>
<script lang="ts"> <script>
import { import {
NcActionButton, NcActionButton,
NcActionLink, NcActionLink,
@ -106,11 +102,10 @@ import {
durationToSeconds, durationToSeconds,
formatEpisodeTimestamp, formatEpisodeTimestamp,
formatLocaleDate, formatLocaleDate,
} from '../../utils/time.ts' } from '../../utils/time.js'
import { hasEnded, isListening } from '../../utils/status.ts' import { hasEnded, isListening } from '../../utils/status.js'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import DownloadIcon from 'vue-material-design-icons/Download.vue' import DownloadIcon from 'vue-material-design-icons/Download.vue'
import type { EpisodeInterface } from '../../utils/types.ts'
import Modal from '../Atoms/Modal.vue' import Modal from '../Atoms/Modal.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue' import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import PlayIcon from 'vue-material-design-icons/Play.vue' import PlayIcon from 'vue-material-design-icons/Play.vue'
@ -118,11 +113,10 @@ import PlaylistPlayIcon from 'vue-material-design-icons/PlaylistPlay.vue'
import PlaylistRemoveIcon from 'vue-material-design-icons/PlaylistRemove.vue' import PlaylistRemoveIcon from 'vue-material-design-icons/PlaylistRemove.vue'
import StopIcon from 'vue-material-design-icons/Stop.vue' import StopIcon from 'vue-material-design-icons/Stop.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { filenameFromUrl } from '../../utils/url.ts' import { filenameFromUrl } from '../../utils/url.js'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { showError } from '../../utils/toast.ts' import { showError } from '../../utils/toast.js'
import { t } from '@nextcloud/l10n' import { usePlayer } from '../../store/player.js'
import { usePlayer } from '../../store/player.ts'
export default { export default {
name: 'Episode', name: 'Episode',
@ -144,7 +138,7 @@ export default {
}, },
props: { props: {
episode: { episode: {
type: Object as () => EpisodeInterface, type: Object,
required: true, required: true,
}, },
oneLine: { oneLine: {
@ -158,22 +152,21 @@ export default {
}, },
data: () => ({ data: () => ({
loading: false, loading: false,
modalEpisode: null as EpisodeInterface | null, modalEpisode: null,
}), }),
computed: { computed: {
...mapState(usePlayer, { playerEpisode: 'episode' }), ...mapState(usePlayer, { playerEpisode: 'episode' }),
}, },
methods: { methods: {
...mapActions(usePlayer, ['load']), ...mapActions(usePlayer, ['load']),
filenameFromUrl,
formatLocaleDate, formatLocaleDate,
hasEnded, hasEnded,
isListening, isListening,
t, filenameFromUrl,
isCurrentEpisode(episode: EpisodeInterface) { isCurrentEpisode(episode) {
return this.playerEpisode?.url === episode.url return this.playerEpisode?.url === episode.url
}, },
async markAs(episode: EpisodeInterface, read: boolean) { async markAs(episode, read) {
try { try {
this.loading = true this.loading = true
episode.action = { episode.action = {
@ -183,8 +176,8 @@ export default {
action: 'play', action: 'play',
timestamp: formatEpisodeTimestamp(new Date()), timestamp: formatEpisodeTimestamp(new Date()),
started: episode.action?.started || 0, started: episode.action?.started || 0,
position: read ? durationToSeconds(episode.duration || '') : 0, position: read ? durationToSeconds(episode.duration) : 0,
total: durationToSeconds(episode.duration || ''), total: durationToSeconds(episode.duration),
} }
await axios.post( await axios.post(
generateUrl('/apps/gpoddersync/episode_action/create'), generateUrl('/apps/gpoddersync/episode_action/create'),

View File

@ -11,19 +11,17 @@
</div> </div>
</template> </template>
<script lang="ts"> <script>
import { hasEnded, isListening } from '../../utils/status.ts' import { hasEnded, isListening } from '../../utils/status.js'
import Episode from './Episode.vue' import Episode from './Episode.vue'
import type { EpisodeInterface } from '../../utils/types.ts'
import Loading from '../Atoms/Loading.vue' import Loading from '../Atoms/Loading.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { decodeUrl } from '../../utils/url.ts' import { decodeUrl } from '../../utils/url.js'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { showError } from '../../utils/toast.ts' import { showError } from '../../utils/toast.js'
import { t } from '@nextcloud/l10n' import { usePlayer } from '../../store/player.js'
import { usePlayer } from '../../store/player.ts' import { useSettings } from '../../store/settings.js'
import { useSettings } from '../../store/settings.ts'
export default { export default {
name: 'Episodes', name: 'Episodes',
@ -32,7 +30,7 @@ export default {
Loading, Loading,
}, },
data: () => ({ data: () => ({
episodes: [] as EpisodeInterface[], episodes: [],
loading: true, loading: true,
}), }),
computed: { computed: {
@ -43,24 +41,27 @@ export default {
if (!this.filters.listened && this.hasEnded(episode)) { if (!this.filters.listened && this.hasEnded(episode)) {
return false return false
} }
if (!this.filters.listening && this.isListening(episode)) { if (!this.filters.listening && this.isListening(episode)) {
return false return false
} }
if (!this.filters.unlistened && !this.isListening(episode)) { if (!this.filters.unlistened && !this.isListening(episode)) {
return false return false
} }
return true return true
}) })
}, },
url() { url() {
return decodeUrl(this.$route.params.url as string) return decodeUrl(this.$route.params.url)
}, },
}, },
watch: { watch: {
episode() { episode() {
if (this.episode) { if (this.episode) {
this.episodes = this.episodes.map((e) => this.episodes = this.episodes.map((e) =>
e.url === this.episode?.url ? this.episode : e, e.url === this.episode.url ? this.episode : e,
) )
} }
}, },
@ -68,15 +69,13 @@ export default {
async mounted() { async mounted() {
try { try {
this.loading = true this.loading = true
const episodes = await axios.get<EpisodeInterface[]>( const episodes = await axios.get(
generateUrl('/apps/repod/episodes/list?url={url}', { generateUrl('/apps/repod/episodes/list?url={url}', {
url: this.url, url: this.url,
}), }),
) )
this.episodes = [...episodes.data].sort( this.episodes = [...episodes.data].sort(
(a, b) => (a, b) => new Date(b.pubDate?.date) - new Date(a.pubDate?.date),
new Date(b.pubDate?.date || '').getTime() -
new Date(a.pubDate?.date || '').getTime(),
) )
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View File

@ -1,15 +1,15 @@
<template> <template>
<NcGuestContent class="guest"> <NcGuestContent class="guest">
<Loading v-if="!feed.data" /> <Loading v-if="!currentFavoriteData" />
<NcAvatar <NcAvatar
v-if="feed.data" v-if="currentFavoriteData"
class="avatar" class="avatar"
:display-name="feed.data.author || feed.data.title" :display-name="currentFavoriteData.author || currentFavoriteData.title"
:is-no-user="true" :is-no-user="true"
:size="222" :size="222"
:url="feed.data.imageUrl" /> :url="currentFavoriteData.imageUrl" />
<div v-if="feed.data" class="list"> <div class="list">
<h2 class="title">{{ feed.data.title }}</h2> <h2 class="title">{{ currentFavoriteData.title }}</h2>
<Loading v-if="loading" /> <Loading v-if="loading" />
<ul v-if="!loading"> <ul v-if="!loading">
<Episode <Episode
@ -17,22 +17,22 @@
:key="episode.guid" :key="episode.guid"
:episode="episode" :episode="episode"
:one-line="true" :one-line="true"
:url="feed.metrics.url" /> :url="url" />
</ul> </ul>
</div> </div>
</NcGuestContent> </NcGuestContent>
</template> </template>
<script lang="ts"> <script>
import type { EpisodeInterface, SubscriptionInterface } from '../../utils/types.ts'
import { NcAvatar, NcGuestContent } from '@nextcloud/vue' import { NcAvatar, NcGuestContent } from '@nextcloud/vue'
import Episode from './Episode.vue' import Episode from './Episode.vue'
import Loading from '../Atoms/Loading.vue' import Loading from '../Atoms/Loading.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { hasEnded } from '../../utils/status.ts' import { hasEnded } from '../../utils/status.js'
import { showError } from '../../utils/toast.ts' import { mapState } from 'pinia'
import { t } from '@nextcloud/l10n' import { showError } from '../../utils/toast.js'
import { useSubscriptions } from '../../store/subscriptions.js'
export default { export default {
name: 'Favorite', name: 'Favorite',
@ -43,28 +43,32 @@ export default {
NcGuestContent, NcGuestContent,
}, },
props: { props: {
feed: { url: {
type: Object as () => SubscriptionInterface, type: String,
required: true, required: true,
}, },
}, },
data: () => ({ data: () => ({
episodes: [] as EpisodeInterface[], episodes: [],
loading: true, loading: true,
}), }),
computed: {
...mapState(useSubscriptions, ['getFavorites']),
currentFavoriteData() {
return this.getFavorites.find((fav) => fav.url === this.url)
},
},
async mounted() { async mounted() {
try { try {
this.loading = true this.loading = true
const episodes = await axios.get<EpisodeInterface[]>( const episodes = await axios.get(
generateUrl('/apps/repod/episodes/list?url={url}', { generateUrl('/apps/repod/episodes/list?url={url}', {
url: this.feed.metrics.url, url: this.url,
}), }),
) )
this.episodes = [...episodes.data] this.episodes = [...episodes.data]
.sort( .sort(
(a, b) => (a, b) => new Date(b.pubDate?.date) - new Date(a.pubDate?.date),
new Date(b.pubDate?.date || '').getTime() -
new Date(a.pubDate?.date || '').getTime(),
) )
.filter((episode) => !this.hasEnded(episode)) .filter((episode) => !this.hasEnded(episode))
.slice(0, 4) .slice(0, 4)

View File

@ -13,7 +13,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script>
import Controls from './Controls.vue' import Controls from './Controls.vue'
import Infos from './Infos.vue' import Infos from './Infos.vue'
import Loading from '../Atoms/Loading.vue' import Loading from '../Atoms/Loading.vue'
@ -21,7 +21,7 @@ import ProgressBar from './ProgressBar.vue'
import Timer from './Timer.vue' import Timer from './Timer.vue'
import Volume from './Volume.vue' import Volume from './Volume.vue'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.js'
export default { export default {
name: 'Bar', name: 'Bar',

View File

@ -5,11 +5,11 @@
</div> </div>
</template> </template>
<script lang="ts"> <script>
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import PauseIcon from 'vue-material-design-icons/Pause.vue' import PauseIcon from 'vue-material-design-icons/Pause.vue'
import PlayIcon from 'vue-material-design-icons/Play.vue' import PlayIcon from 'vue-material-design-icons/Play.vue'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.js'
export default { export default {
name: 'Controls', name: 'Controls',

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-if="episode && podcastUrl" class="root"> <div class="root">
<strong class="pointer" @click="modal = true"> <strong class="pointer" @click="modal = true">
{{ episode.name }} {{ episode.name }}
</strong> </strong>
@ -12,12 +12,12 @@
</div> </div>
</template> </template>
<script lang="ts"> <script>
import Modal from '../Atoms/Modal.vue' import Modal from '../Atoms/Modal.vue'
import { NcModal } from '@nextcloud/vue' import { NcModal } from '@nextcloud/vue'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { toFeedUrl } from '../../utils/url.ts' import { toFeedUrl } from '../../utils/url.js'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.js'
export default { export default {
name: 'Infos', name: 'Infos',

View File

@ -1,19 +1,16 @@
<template> <template>
<input <input
v-if="duration"
class="progress" class="progress"
:max="duration" :max="duration"
min="0" min="0"
type="range" type="range"
:value="currentTime" :value="currentTime"
@change=" @change="(event) => seek(event.target.value)" />
(event) => seek(parseInt((event.target as HTMLInputElement).value))
" />
</template> </template>
<script lang="ts"> <script>
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.js'
export default { export default {
name: 'ProgressBar', name: 'ProgressBar',

View File

@ -1,15 +1,15 @@
<template> <template>
<div v-if="currentTime && duration" class="root"> <div class="root">
<span>{{ formatTimer(new Date(currentTime * 1000)) }}</span> <span>{{ formatTimer(new Date(currentTime * 1000)) }}</span>
<span>/</span> <span>/</span>
<span>{{ formatTimer(new Date(duration * 1000)) }}</span> <span>{{ formatTimer(new Date(duration * 1000)) }}</span>
</div> </div>
</template> </template>
<script lang="ts"> <script>
import { formatTimer } from '../../utils/time.ts' import { formatTimer } from '../../utils/time.js'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.js'
export default { export default {
name: 'Timer', name: 'Timer',

View File

@ -26,20 +26,17 @@
step="0.1" step="0.1"
type="range" type="range"
:value="volume" :value="volume"
@change=" @change="(event) => setVolume(event.target.value)" />
(event) =>
setVolume(parseInt((event.target as HTMLInputElement).value))
" />
</div> </div>
</template> </template>
<script lang="ts"> <script>
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import VolumeHighIcon from 'vue-material-design-icons/VolumeHigh.vue' import VolumeHighIcon from 'vue-material-design-icons/VolumeHigh.vue'
import VolumeLowIcon from 'vue-material-design-icons/VolumeLow.vue' import VolumeLowIcon from 'vue-material-design-icons/VolumeLow.vue'
import VolumeMediumIcon from 'vue-material-design-icons/VolumeMedium.vue' import VolumeMediumIcon from 'vue-material-design-icons/VolumeMedium.vue'
import VolumeMuteIcon from 'vue-material-design-icons/VolumeMute.vue' import VolumeMuteIcon from 'vue-material-design-icons/VolumeMute.vue'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.js'
export default { export default {
name: 'Volume', name: 'Volume',

View File

@ -8,11 +8,10 @@
</NcAppNavigationItem> </NcAppNavigationItem>
</template> </template>
<script lang="ts"> <script>
import ExportIcon from 'vue-material-design-icons/Export.vue' import ExportIcon from 'vue-material-design-icons/Export.vue'
import { NcAppNavigationItem } from '@nextcloud/vue' import { NcAppNavigationItem } from '@nextcloud/vue'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { t } from '@nextcloud/l10n'
export default { export default {
name: 'Export', name: 'Export',
@ -22,7 +21,6 @@ export default {
}, },
methods: { methods: {
generateUrl, generateUrl,
t,
}, },
} }
</script> </script>

View File

@ -39,13 +39,12 @@
</NcAppNavigationItem> </NcAppNavigationItem>
</template> </template>
<script lang="ts"> <script>
import { NcActionCheckbox, NcAppNavigationItem } from '@nextcloud/vue' import { NcActionCheckbox, NcAppNavigationItem } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import FilterIcon from 'vue-material-design-icons/Filter.vue' import FilterIcon from 'vue-material-design-icons/Filter.vue'
import FilterSettingsIcon from 'vue-material-design-icons/FilterSettings.vue' import FilterSettingsIcon from 'vue-material-design-icons/FilterSettings.vue'
import { t } from '@nextcloud/l10n' import { useSettings } from '../../store/settings.js'
import { useSettings } from '../../store/settings.ts'
export default { export default {
name: 'Filters', name: 'Filters',
@ -67,7 +66,6 @@ export default {
}, },
methods: { methods: {
...mapActions(useSettings, ['setFilters']), ...mapActions(useSettings, ['setFilters']),
t,
}, },
} }
</script> </script>

View File

@ -29,13 +29,12 @@
</NcAppNavigationItem> </NcAppNavigationItem>
</template> </template>
<script lang="ts"> <script>
import { NcAppNavigationItem, NcModal } from '@nextcloud/vue' import { NcAppNavigationItem, NcModal } from '@nextcloud/vue'
import ImportIcon from 'vue-material-design-icons/Import.vue' import ImportIcon from 'vue-material-design-icons/Import.vue'
import Loading from '../Atoms/Loading.vue' import Loading from '../Atoms/Loading.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { t } from '@nextcloud/l10n'
export default { export default {
name: 'Import', name: 'Import',
@ -51,13 +50,11 @@ export default {
}), }),
methods: { methods: {
generateUrl, generateUrl,
t, async importOpml(event) {
async importOpml(event: Event) {
try { try {
const target = event.target as HTMLFormElement const formData = new FormData(event.target)
const formData = new FormData(target) this.importLoading = true
this.loading = true await axios.post(event.target.action, formData)
await axios.post(target.action, formData)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally { } finally {

View File

@ -8,10 +8,9 @@
</NcAppNavigationItem> </NcAppNavigationItem>
</template> </template>
<script lang="ts"> <script>
import { NcAppNavigationItem } from '@nextcloud/vue' import { NcAppNavigationItem } from '@nextcloud/vue'
import StarIcon from 'vue-material-design-icons/Star.vue' import StarIcon from 'vue-material-design-icons/Star.vue'
import { t } from '@nextcloud/l10n'
export default { export default {
name: 'Rate', name: 'Rate',
@ -19,8 +18,5 @@ export default {
NcAppNavigationItem, NcAppNavigationItem,
StarIcon, StarIcon,
}, },
methods: {
t,
},
} }
</script> </script>

View File

@ -8,7 +8,7 @@
</NcAppNavigationSettings> </NcAppNavigationSettings>
</template> </template>
<script lang="ts"> <script>
import Export from './Export.vue' import Export from './Export.vue'
import Filters from './Filters.vue' import Filters from './Filters.vue'
import Import from './Import.vue' import Import from './Import.vue'

View File

@ -15,7 +15,7 @@
</NcAppNavigationItem> </NcAppNavigationItem>
</template> </template>
<script lang="ts"> <script>
import { NcAppNavigationItem, NcCounterBubble } from '@nextcloud/vue' import { NcAppNavigationItem, NcCounterBubble } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import MinusIcon from 'vue-material-design-icons/Minus.vue' import MinusIcon from 'vue-material-design-icons/Minus.vue'
@ -23,8 +23,7 @@ import PlusIcon from 'vue-material-design-icons/Plus.vue'
import SpeedometerIcon from 'vue-material-design-icons/Speedometer.vue' import SpeedometerIcon from 'vue-material-design-icons/Speedometer.vue'
import SpeedometerMediumIcon from 'vue-material-design-icons/SpeedometerMedium.vue' import SpeedometerMediumIcon from 'vue-material-design-icons/SpeedometerMedium.vue'
import SpeedometerSlowIcon from 'vue-material-design-icons/SpeedometerSlow.vue' import SpeedometerSlowIcon from 'vue-material-design-icons/SpeedometerSlow.vue'
import { t } from '@nextcloud/l10n' import { usePlayer } from '../../store/player.js'
import { usePlayer } from '../../store/player.ts'
export default { export default {
name: 'Speed', name: 'Speed',
@ -42,9 +41,8 @@ export default {
}, },
methods: { methods: {
...mapActions(usePlayer, ['setRate']), ...mapActions(usePlayer, ['setRate']),
t, changeRate(diff) {
changeRate(diff: number) { const newRate = (this.rate + diff).toPrecision(2)
const newRate = parseFloat((this.rate + diff).toPrecision(2))
this.setRate(newRate > 0 ? newRate : this.rate) this.setRate(newRate > 0 ? newRate : this.rate)
}, },
}, },

View File

@ -1,18 +1,18 @@
<template> <template>
<NcAppNavigationItem <NcAppNavigationItem
:loading="loading" :loading="loading"
:name="feed?.data?.title || url" :name="feed ? feed.title : url"
:to="toFeedUrl(url)"> :to="toFeedUrl(url)">
<template #actions> <template #actions>
<NcActionButton <NcActionButton
:aria-label="t('repod', 'Favorite')" :aria-label="t('repod', 'Favorite')"
:model-value="feed?.isFavorite" :model-value="isFavorite"
:name="t('repod', 'Favorite')" :name="t('repod', 'Favorite')"
:title="t('repod', 'Favorite')" :title="t('repod', 'Favorite')"
@update:modelValue="switchFavorite($event)"> @update:modelValue="switchFavorite($event)">
<template #icon> <template #icon>
<StarPlusIcon v-if="!feed?.isFavorite" :size="20" /> <StarPlusIcon v-if="!isFavorite" :size="20" />
<StarRemoveIcon v-if="feed?.isFavorite" :size="20" /> <StarRemoveIcon v-if="isFavorite" :size="20" />
</template> </template>
</NcActionButton> </NcActionButton>
<NcActionButton <NcActionButton
@ -27,30 +27,29 @@
</template> </template>
<template #icon> <template #icon>
<NcAvatar <NcAvatar
:display-name="feed?.data?.author || feed?.data?.title" v-if="feed"
:display-name="feed.author || feed.title"
:is-no-user="true" :is-no-user="true"
:url="feed?.data?.imageUrl" /> :url="feed.imageUrl" />
<StarIcon v-if="feed?.isFavorite" class="star" :size="20" /> <StarIcon v-if="feed && isFavorite" class="star" :size="20" />
<AlertIcon v-if="failed" /> <AlertIcon v-if="failed" />
</template> </template>
</NcAppNavigationItem> </NcAppNavigationItem>
</template> </template>
<script lang="ts"> <script>
import { NcActionButton, NcAppNavigationItem, NcAvatar } from '@nextcloud/vue' import { NcActionButton, NcAppNavigationItem, NcAvatar } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import AlertIcon from 'vue-material-design-icons/Alert.vue' import AlertIcon from 'vue-material-design-icons/Alert.vue'
import DeleteIcon from 'vue-material-design-icons/Delete.vue' import DeleteIcon from 'vue-material-design-icons/Delete.vue'
import type { PersonalSettingsPodcastDataInterface } from '../../utils/types.ts'
import StarIcon from 'vue-material-design-icons/Star.vue' import StarIcon from 'vue-material-design-icons/Star.vue'
import StarPlusIcon from 'vue-material-design-icons/StarPlus.vue' import StarPlusIcon from 'vue-material-design-icons/StarPlus.vue'
import StarRemoveIcon from 'vue-material-design-icons/StarRemove.vue' import StarRemoveIcon from 'vue-material-design-icons/StarRemove.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { showError } from '../../utils/toast.ts' import { showError } from '../../utils/toast.js'
import { t } from '@nextcloud/l10n' import { toFeedUrl } from '../../utils/url.js'
import { toFeedUrl } from '../../utils/url.ts' import { useSubscriptions } from '../../store/subscriptions.js'
import { useSubscriptions } from '../../store/subscriptions.ts'
export default { export default {
name: 'Subscription', name: 'Subscription',
@ -73,17 +72,17 @@ export default {
data: () => ({ data: () => ({
failed: false, failed: false,
loading: true, loading: true,
feed: null,
}), }),
computed: { computed: {
...mapState(useSubscriptions, ['subs']), ...mapState(useSubscriptions, ['getFavorites']),
feed() { isFavorite() {
return this.subs.find((sub) => sub.metrics.url === this.url) return this.getFavorites.map((fav) => fav.url).includes(this.url)
}, },
}, },
async mounted() { async mounted() {
try { try {
const podcastData = const podcastData = await axios.get(
await axios.get<PersonalSettingsPodcastDataInterface>(
generateUrl( generateUrl(
'/apps/gpoddersync/personal_settings/podcast_data?url={url}', '/apps/gpoddersync/personal_settings/podcast_data?url={url}',
{ {
@ -91,7 +90,8 @@ export default {
}, },
), ),
) )
this.addMetadatas(this.url, podcastData.data.data) this.feed = podcastData.data.data
this.editFavoriteData(this.url, podcastData.data.data)
} catch (e) { } catch (e) {
this.failed = true this.failed = true
console.error(e) console.error(e)
@ -100,8 +100,12 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(useSubscriptions, ['fetch', 'addMetadatas', 'setFavorite']), ...mapActions(useSubscriptions, [
t, 'fetch',
'addFavorite',
'editFavoriteData',
'removeFavorite',
]),
toFeedUrl, toFeedUrl,
async deleteSubscription() { async deleteSubscription() {
if ( if (
@ -119,20 +123,23 @@ export default {
console.error(e) console.error(e)
showError(t('repod', 'Error while removing the feed')) showError(t('repod', 'Error while removing the feed'))
} finally { } finally {
this.setFavorite(this.url, false) this.removeFavorite(this.url)
this.loading = false this.loading = false
this.fetch() this.fetch()
} }
} }
}, },
switchFavorite(value: boolean) { switchFavorite(value) {
if (value) { if (value) {
if (this.subs.filter((sub) => sub.isFavorite).length >= 10) { if (this.getFavorites.length >= 10) {
showError(t('repod', 'You can only have 10 favorites')) showError(t('repod', 'You can only have 10 favorites'))
return return
} }
this.addFavorite(this.url)
} else {
this.removeFavorite(this.url)
} }
this.setFavorite(this.url, value)
}, },
}, },
} }

View File

@ -12,13 +12,16 @@
<Loading v-if="loading" /> <Loading v-if="loading" />
<NcAppNavigationList v-if="!loading"> <NcAppNavigationList v-if="!loading">
<Subscription <Subscription
v-for="sub of subs.filter((sub) => sub.isFavorite)" v-for="url of getFavorites.map((fav) => fav.url)"
:key="sub.metrics.url" :key="url"
:url="sub.metrics.url" /> :url="url" />
<Subscription <Subscription
v-for="sub of subs.filter((sub) => !sub.isFavorite)" v-for="url of getSubscriptions.filter(
:key="sub.metrics.url" (sub) =>
:url="sub.metrics.url" /> !getFavorites.map((fav) => fav.url).includes(sub),
)"
:key="url"
:url="url" />
</NcAppNavigationList> </NcAppNavigationList>
</NcAppContentList> </NcAppContentList>
</template> </template>
@ -28,7 +31,7 @@
</AppNavigation> </AppNavigation>
</template> </template>
<script lang="ts"> <script>
import { import {
NcAppContentList, NcAppContentList,
NcAppNavigationList, NcAppNavigationList,
@ -40,9 +43,8 @@ import Loading from '../Atoms/Loading.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue' import PlusIcon from 'vue-material-design-icons/Plus.vue'
import Settings from '../Settings/Settings.vue' import Settings from '../Settings/Settings.vue'
import Subscription from './Subscription.vue' import Subscription from './Subscription.vue'
import { showError } from '../../utils/toast.ts' import { showError } from '../../utils/toast.js'
import { t } from '@nextcloud/l10n' import { useSubscriptions } from '../../store/subscriptions.js'
import { useSubscriptions } from '../../store/subscriptions.ts'
export default { export default {
name: 'Subscriptions', name: 'Subscriptions',
@ -60,7 +62,7 @@ export default {
loading: true, loading: true,
}), }),
computed: { computed: {
...mapState(useSubscriptions, ['subs']), ...mapState(useSubscriptions, ['getSubscriptions', 'getFavorites']),
}, },
async mounted() { async mounted() {
try { try {
@ -74,7 +76,6 @@ export default {
}, },
methods: { methods: {
...mapActions(useSubscriptions, ['fetch']), ...mapActions(useSubscriptions, ['fetch']),
t,
}, },
} }
</script> </script>

View File

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

View File

@ -1,19 +1,18 @@
import type { EpisodeActionInterface, EpisodeInterface } from '../utils/types.ts'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { formatEpisodeTimestamp } from '../utils/time.ts' import { formatEpisodeTimestamp } from '../utils/time.js'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
const audio = new Audio() const audio = new Audio()
export const usePlayer = defineStore('player', { export const usePlayer = defineStore('player', {
state: () => ({ state: () => ({
currentTime: null as number | null, currentTime: null,
duration: null as number | null, duration: null,
episode: null as EpisodeInterface | null, episode: null,
loaded: false, loaded: false,
paused: true, paused: null,
podcastUrl: null as string | null, podcastUrl: null,
volume: 1, volume: 1,
rate: 1, rate: 1,
started: 0, started: 0,
@ -30,16 +29,16 @@ export const usePlayer = defineStore('player', {
audio.ontimeupdate = () => (this.currentTime = audio.currentTime) audio.ontimeupdate = () => (this.currentTime = audio.currentTime)
audio.onvolumechange = () => (this.volume = audio.volume) audio.onvolumechange = () => (this.volume = audio.volume)
}, },
async load(episode: EpisodeInterface | null, podcastUrl?: string) { async load(episode, podcastUrl) {
this.episode = episode this.episode = episode
this.podcastUrl = podcastUrl || null this.podcastUrl = podcastUrl
if (this.episode?.url) { if (this.episode) {
audio.src = this.episode.url audio.src = this.episode.url
audio.load() audio.load()
try { try {
const action = await axios.get<EpisodeActionInterface>( const action = await axios.get(
generateUrl('/apps/repod/episodes/action?url={url}', { generateUrl('/apps/repod/episodes/action?url={url}', {
url: this.episode.url, url: this.episode.url,
}), }),
@ -73,7 +72,7 @@ export const usePlayer = defineStore('player', {
this.paused = false this.paused = false
this.started = audio.currentTime this.started = audio.currentTime
}, },
seek(currentTime: number) { seek(currentTime) {
audio.currentTime = currentTime audio.currentTime = currentTime
this.time() this.time()
}, },
@ -82,10 +81,6 @@ export const usePlayer = defineStore('player', {
this.episode = null this.episode = null
}, },
time() { time() {
if (!this.podcastUrl || !this.episode?.url) {
return
}
this.episode.action = { this.episode.action = {
podcast: this.podcastUrl, podcast: this.podcastUrl,
episode: this.episode.url, episode: this.episode.url,
@ -96,15 +91,14 @@ export const usePlayer = defineStore('player', {
position: Math.round(audio.currentTime), position: Math.round(audio.currentTime),
total: Math.round(audio.duration), total: Math.round(audio.duration),
} }
axios.post(generateUrl('/apps/gpoddersync/episode_action/create'), [ axios.post(generateUrl('/apps/gpoddersync/episode_action/create'), [
this.episode.action, this.episode.action,
]) ])
}, },
setVolume(volume: number) { setVolume(volume) {
audio.volume = volume audio.volume = volume
}, },
setRate(rate: number) { setRate(rate) {
audio.playbackRate = rate audio.playbackRate = rate
}, },
}, },

View File

@ -1,16 +1,10 @@
import { getCookie, setCookie } from '../utils/cookies.ts' import { getCookie, setCookie } from '../utils/cookies.js'
import type { FiltersInterface } from '../utils/types.ts'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
export const useSettings = defineStore('settings', { export const useSettings = defineStore('settings', {
state: () => { state: () => {
try { try {
const filters = JSON.parse(getCookie('repod.filters') || '{}') || {} const filters = JSON.parse(getCookie('repod.filters'))
if (!filters.length) {
throw new Error('Empty cookie')
}
return { return {
filters: { filters: {
listened: filters.listened, listened: filters.listened,
@ -29,7 +23,7 @@ export const useSettings = defineStore('settings', {
} }
}, },
actions: { actions: {
setFilters(filters: Partial<FiltersInterface>) { setFilters(filters) {
this.filters = { ...this.filters, ...filters } this.filters = { ...this.filters, ...filters }
setCookie('repod.filters', JSON.stringify(this.filters), 365) setCookie('repod.filters', JSON.stringify(this.filters), 365)
}, },

View File

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

View File

@ -1,59 +0,0 @@
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,
)
},
},
})

View File

@ -4,7 +4,7 @@
* @param {string} name Nom du cookie à récupérer * @param {string} name Nom du cookie à récupérer
* @return {string|null} * @return {string|null}
*/ */
export const getCookie = (name: string): string | null => { export const getCookie = (name) => {
const cookies = document.cookie.split('; ') const cookies = document.cookie.split('; ')
const value = cookies.find((c) => c.startsWith(name + '='))?.split('=')[1] const value = cookies.find((c) => c.startsWith(name + '='))?.split('=')[1]
if (value === undefined) { if (value === undefined) {
@ -19,7 +19,7 @@ export const getCookie = (name: string): string | null => {
* @param {string} value Value du cookie * @param {string} value Value du cookie
* @param {number} days Durée de vie du cookie (en jours) * @param {number} days Durée de vie du cookie (en jours)
*/ */
export const setCookie = (name: string, value: string, days: number) => { export const setCookie = (name, value, days) => {
const date = new Date() const date = new Date()
date.setDate(date.getDate() + days) date.setDate(date.getDate() + days)
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${date.toUTCString()}; SameSite=Strict;` document.cookie = `${name}=${encodeURIComponent(value)}; expires=${date.toUTCString()}; SameSite=Strict;`

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

@ -0,0 +1,12 @@
// 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)
}
}

View File

@ -1,8 +1,8 @@
// https://stackoverflow.com/a/20732091 // https://stackoverflow.com/a/20732091
export const humanFileSize = (size: number) => { export const humanFileSize = (size) => {
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)) const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024))
return ( return (
(size / Math.pow(1024, i)).toFixed(2) + (size / Math.pow(1024, i)).toFixed(2) * 1 +
' ' + ' ' +
['B', 'kB', 'MB', 'GB', 'TB'][i] ['B', 'kB', 'MB', 'GB', 'TB'][i]
) )

View File

@ -1,6 +1,4 @@
import type { EpisodeInterface } from './types' export const hasEnded = (episode) =>
export const hasEnded = (episode: EpisodeInterface) =>
episode.action && episode.action &&
episode.action.action && episode.action.action &&
(episode.action.action.toLowerCase() === 'delete' || (episode.action.action.toLowerCase() === 'delete' ||
@ -8,7 +6,7 @@ export const hasEnded = (episode: EpisodeInterface) =>
episode.action.total > 0 && episode.action.total > 0 &&
episode.action.position >= episode.action.total)) episode.action.position >= episode.action.total))
export const isListening = (episode: EpisodeInterface) => export const isListening = (episode) =>
episode.action && episode.action &&
episode.action.action && episode.action.action &&
episode.action.action.toLowerCase() === 'play' && episode.action.action.toLowerCase() === 'play' &&

View File

@ -3,9 +3,9 @@
* @param {Date} date The date * @param {Date} date The date
* @return {string} * @return {string}
*/ */
export const formatTimer = (date: Date): string => { export const formatTimer = (date) => {
const minutes = date.getUTCMinutes().toString().padStart(2, '0') const minutes = date.getUTCMinutes().toString().padStart(2, 0)
const seconds = date.getUTCSeconds().toString().padStart(2, '0') const seconds = date.getUTCSeconds().toString().padStart(2, 0)
let timer = `${minutes}:${seconds}` let timer = `${minutes}:${seconds}`
if (date.getUTCHours()) { if (date.getUTCHours()) {
@ -20,7 +20,7 @@ export const formatTimer = (date: Date): string => {
* @param {Date} date The date * @param {Date} date The date
* @return {string} * @return {string}
*/ */
export const formatEpisodeTimestamp = (date: Date): string => { export const formatEpisodeTimestamp = (date) => {
const year = date.getFullYear() const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0') const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0') const day = date.getDate().toString().padStart(2, '0')
@ -36,7 +36,7 @@ export const formatEpisodeTimestamp = (date: Date): string => {
* @param {Date} date The date * @param {Date} date The date
* @return {string} * @return {string}
*/ */
export const formatLocaleDate = (date: Date): string => export const formatLocaleDate = (date) =>
date.toLocaleDateString(undefined, { dateStyle: 'medium' }) date.toLocaleDateString(undefined, { dateStyle: 'medium' })
/** /**
@ -44,7 +44,7 @@ export const formatLocaleDate = (date: Date): string =>
* @param {string} duration The duration feed's entry * @param {string} duration The duration feed's entry
* @return {number} * @return {number}
*/ */
export const durationToSeconds = (duration: string): number => { export const durationToSeconds = (duration) => {
const splitDuration = duration.split(':').reverse() const splitDuration = duration.split(':').reverse()
let seconds = parseInt(splitDuration[0]) let seconds = parseInt(splitDuration[0])
seconds += splitDuration.length > 1 ? parseInt(splitDuration[1]) * 60 : 0 seconds += splitDuration.length > 1 ? parseInt(splitDuration[1]) * 60 : 0

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

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

View File

@ -1,14 +0,0 @@
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)')

View File

@ -1,73 +0,0 @@
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
}

4
src/utils/url.js Normal file
View File

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

View File

@ -1,5 +0,0 @@
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()

View File

@ -12,14 +12,13 @@
</AppContent> </AppContent>
</template> </template>
<script lang="ts"> <script>
import AddRss from '../components/Discover/AddRss.vue' import AddRss from '../components/Discover/AddRss.vue'
import AppContent from '../components/Atoms/AppContent.vue' import AppContent from '../components/Atoms/AppContent.vue'
import Magnify from 'vue-material-design-icons/Magnify.vue' import Magnify from 'vue-material-design-icons/Magnify.vue'
import { NcTextField } from '@nextcloud/vue' import { NcTextField } from '@nextcloud/vue'
import Search from '../components/Discover/Search.vue' import Search from '../components/Discover/Search.vue'
import Toplist from '../components/Discover/Toplist.vue' import Toplist from '../components/Discover/Toplist.vue'
import { t } from '@nextcloud/l10n'
export default { export default {
name: 'Discover', name: 'Discover',
@ -34,9 +33,6 @@ export default {
data: () => ({ data: () => ({
search: '', search: '',
}), }),
methods: {
t,
},
} }
</script> </script>

View File

@ -6,23 +6,27 @@
<Alert /> <Alert />
</template> </template>
</EmptyContent> </EmptyContent>
<Banner v-if="feed" :feed="feed" /> <Banner
v-if="feed"
:author="feed.author"
:description="feed.description"
:image-url="feed.imageUrl"
:link="feed.link"
:title="feed.title" />
<Episodes v-if="feed" /> <Episodes v-if="feed" />
</AppContent> </AppContent>
</template> </template>
<script lang="ts"> <script>
import Alert from 'vue-material-design-icons/Alert.vue' import Alert from 'vue-material-design-icons/Alert.vue'
import AppContent from '../components/Atoms/AppContent.vue' import AppContent from '../components/Atoms/AppContent.vue'
import Banner from '../components/Feed/Banner.vue' import Banner from '../components/Feed/Banner.vue'
import EmptyContent from '../components/Atoms/EmptyContent.vue' import EmptyContent from '../components/Atoms/EmptyContent.vue'
import Episodes from '../components/Feed/Episodes.vue' import Episodes from '../components/Feed/Episodes.vue'
import Loading from '../components/Atoms/Loading.vue' import Loading from '../components/Atoms/Loading.vue'
import type { PodcastDataInterface } from '../utils/types.ts'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { decodeUrl } from '../utils/url.ts' import { decodeUrl } from '../utils/url.js'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { t } from '@nextcloud/l10n'
export default { export default {
name: 'Feed', name: 'Feed',
@ -37,17 +41,16 @@ export default {
data: () => ({ data: () => ({
failed: false, failed: false,
loading: true, loading: true,
feed: null as PodcastDataInterface | null, feed: null,
}), }),
computed: { computed: {
url() { url() {
return decodeUrl(this.$route.params.url as string) return decodeUrl(this.$route.params.url)
}, },
}, },
async mounted() { async mounted() {
try { try {
this.loading = true const podcastData = await axios.get(
const podcastData = await axios.get<PodcastDataInterface>(
generateUrl('/apps/repod/podcast?url={url}', { url: this.url }), generateUrl('/apps/repod/podcast?url={url}', { url: this.url }),
) )
this.feed = podcastData.data this.feed = podcastData.data
@ -58,8 +61,5 @@ export default {
this.loading = false this.loading = false
} }
}, },
methods: {
t,
},
} }
</script> </script>

View File

@ -13,13 +13,12 @@
</AppContent> </AppContent>
</template> </template>
<script lang="ts"> <script>
import Alert from 'vue-material-design-icons/Alert.vue' import Alert from 'vue-material-design-icons/Alert.vue'
import AppContent from '../components/Atoms/AppContent.vue' import AppContent from '../components/Atoms/AppContent.vue'
import EmptyContent from '../components/Atoms/EmptyContent.vue' import EmptyContent from '../components/Atoms/EmptyContent.vue'
import { NcButton } from '@nextcloud/vue' import { NcButton } from '@nextcloud/vue'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { t } from '@nextcloud/l10n'
export default { export default {
name: 'GPodder', name: 'GPodder',
@ -34,8 +33,5 @@ export default {
return generateUrl('/settings/apps/installed/gpoddersync') return generateUrl('/settings/apps/installed/gpoddersync')
}, },
}, },
methods: {
t,
},
} }
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<AppContent> <AppContent>
<EmptyContent <EmptyContent
v-if="!favorites.length" v-if="!getFavorites.length"
:description=" :description="
t('repod', 'Pin some subscriptions to see their latest updates') t('repod', 'Pin some subscriptions to see their latest updates')
" "
@ -10,22 +10,21 @@
<StarOffIcon /> <StarOffIcon />
</template> </template>
</EmptyContent> </EmptyContent>
<ul v-if="favorites.length"> <ul v-if="getFavorites.length">
<li v-for="favorite in favorites" :key="favorite.metrics.url"> <li v-for="url in getFavorites.map((fav) => fav.url)" :key="url">
<Favorite :feed="favorite" /> <Favorite :url="url" />
</li> </li>
</ul> </ul>
</AppContent> </AppContent>
</template> </template>
<script lang="ts"> <script>
import AppContent from '../components/Atoms/AppContent.vue' import AppContent from '../components/Atoms/AppContent.vue'
import EmptyContent from '../components/Atoms/EmptyContent.vue' import EmptyContent from '../components/Atoms/EmptyContent.vue'
import Favorite from '../components/Feed/Favorite.vue' import Favorite from '../components/Feed/Favorite.vue'
import StarOffIcon from 'vue-material-design-icons/StarOff.vue' import StarOffIcon from 'vue-material-design-icons/StarOff.vue'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { t } from '@nextcloud/l10n' import { useSubscriptions } from '../store/subscriptions.js'
import { useSubscriptions } from '../store/subscriptions.ts'
export default { export default {
name: 'Home', name: 'Home',
@ -36,13 +35,7 @@ export default {
StarOffIcon, StarOffIcon,
}, },
computed: { computed: {
...mapState(useSubscriptions, ['subs']), ...mapState(useSubscriptions, ['getFavorites']),
favorites() {
return this.subs.filter((sub) => sub.isFavorite)
},
},
methods: {
t,
}, },
} }
</script> </script>

View File

@ -1,19 +0,0 @@
{
"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,
},
}

View File

@ -4,6 +4,11 @@ import vueDevTools from 'vite-plugin-vue-devtools'
const config = defineConfig(({ mode }) => ({ const config = defineConfig(({ mode }) => ({
build: { build: {
rollupOptions: {
output: {
entryFileNames: 'js/[name].js',
},
},
sourcemap: mode !== 'production', sourcemap: mode !== 'production',
}, },
define: { define: {
@ -14,7 +19,7 @@ const config = defineConfig(({ mode }) => ({
export default createAppConfig( export default createAppConfig(
{ {
main: 'src/main.ts', main: 'src/main.js',
}, },
{ config, inlineCSS: true }, { config, inlineCSS: true },
) )