typescript #149 #152
@ -1,9 +1,11 @@
|
|||||||
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',
|
@ -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": "^29.0.6",
|
"nextcloud/ocp": "^30.0.0",
|
||||||
"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
26
composer.lock
generated
@ -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": "d1d678bc8d001322808264320c120173",
|
"content-hash": "f21708cb7d3f7f4033053536e295e633",
|
||||||
"packages": [],
|
"packages": [],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
{
|
{
|
||||||
@ -733,16 +733,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nextcloud/ocp",
|
"name": "nextcloud/ocp",
|
||||||
"version": "v29.0.7",
|
"version": "v30.0.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/nextcloud-deps/ocp.git",
|
"url": "https://github.com/nextcloud-deps/ocp.git",
|
||||||
"reference": "b130f11ce24351a6a91115aa6f386271f7aeee9d"
|
"reference": "a26b4e1f75983f359bd835c2529ce37b5599d58f"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/b130f11ce24351a6a91115aa6f386271f7aeee9d",
|
"url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/a26b4e1f75983f359bd835c2529ce37b5599d58f",
|
||||||
"reference": "b130f11ce24351a6a91115aa6f386271f7aeee9d",
|
"reference": "a26b4e1f75983f359bd835c2529ce37b5599d58f",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -755,7 +755,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-stable29": "29.0.0-dev"
|
"dev-stable30": "30.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/v29.0.7"
|
"source": "https://github.com/nextcloud-deps/ocp/tree/v30.0.0"
|
||||||
},
|
},
|
||||||
"time": "2024-09-05T00:40:09+00:00"
|
"time": "2024-09-13T00:40:45+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": "ed0688c3e18bf76d2a17fb243b99acb52c2e29ef"
|
"reference": "fb263701a24214c3176ef23bfa98a7cbc59aa659"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/ed0688c3e18bf76d2a17fb243b99acb52c2e29ef",
|
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/fb263701a24214c3176ef23bfa98a7cbc59aa659",
|
||||||
"reference": "ed0688c3e18bf76d2a17fb243b99acb52c2e29ef",
|
"reference": "fb263701a24214c3176ef23bfa98a7cbc59aa659",
|
||||||
"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.2.1",
|
"phpoffice/phpspreadsheet": "<1.29.1|>=2,<2.1.1|>=2.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-10T18:06:22+00:00"
|
"time": "2024-09-13T14:04:35+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/diff",
|
"name": "sebastian/diff",
|
||||||
|
@ -26,7 +26,7 @@ class PageController extends Controller
|
|||||||
* @NoCSRFRequired
|
* @NoCSRFRequired
|
||||||
*/
|
*/
|
||||||
public function index(): TemplateResponse {
|
public function index(): TemplateResponse {
|
||||||
Util::addScript(Application::APP_ID, 'main');
|
Util::addScript(Application::APP_ID, Application::APP_ID.'-main');
|
||||||
|
|
||||||
$csp = new ContentSecurityPolicy();
|
$csp = new ContentSecurityPolicy();
|
||||||
$csp->addAllowedImageDomain('*');
|
$csp->addAllowedImageDomain('*');
|
||||||
|
639
package-lock.json
generated
639
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "repod",
|
"name": "repod",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build --mode production",
|
"build": "vue-tsc && 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",
|
||||||
@ -11,9 +12,6 @@
|
|||||||
"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",
|
||||||
@ -26,19 +24,29 @@
|
|||||||
"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.3",
|
"vite": "^5.4.5",
|
||||||
"vite-plugin-vue-devtools": "^7.4.4",
|
"vite-plugin-vue-devtools": "^7.4.5",
|
||||||
"vue": "^3.5.4",
|
"vue": "^3.5.5",
|
||||||
"vue-material-design-icons": "^5.3.0",
|
"vue-material-design-icons": "^5.3.0",
|
||||||
"vue-router": "^4.4.4"
|
"vue-router": "^4.4.5"
|
||||||
},
|
},
|
||||||
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</NcContent>
|
</NcContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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.js'
|
import { usePlayer } from './store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
</NcAppContent>
|
</NcAppContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { NcAppContent } from '@nextcloud/vue'
|
import { NcAppContent } from '@nextcloud/vue'
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
import { usePlayer } from '../../store/player.js'
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AppContent',
|
name: 'AppContent',
|
||||||
|
@ -10,10 +10,10 @@
|
|||||||
</NcAppNavigation>
|
</NcAppNavigation>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { NcAppNavigation } from '@nextcloud/vue'
|
import { NcAppNavigation } from '@nextcloud/vue'
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
import { usePlayer } from '../../store/player.js'
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AppNavigation',
|
name: 'AppNavigation',
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
</NcEmptyContent>
|
</NcEmptyContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { NcEmptyContent } from '@nextcloud/vue'
|
import { NcEmptyContent } from '@nextcloud/vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<NcLoadingIcon class="loading" />
|
<NcLoadingIcon class="loading" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { NcLoadingIcon } from '@nextcloud/vue'
|
import { NcLoadingIcon } from '@nextcloud/vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -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,13 +29,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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.js'
|
import { filenameFromUrl } from '../../utils/url.ts'
|
||||||
import { humanFileSize } from '../../utils/size.js'
|
import { humanFileSize } from '../../utils/size.ts'
|
||||||
|
import { t } from '@nextcloud/l10n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Modal',
|
name: 'Modal',
|
||||||
@ -48,13 +50,14 @@ export default {
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
episode: {
|
episode: {
|
||||||
type: Object,
|
type: Object as () => EpisodeInterface,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
filenameFromUrl,
|
filenameFromUrl,
|
||||||
humanFileSize,
|
humanFileSize,
|
||||||
|
t,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div v-sanitize="source" class="html" />
|
<div v-sanitize="source" class="html" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import dompurify from 'dompurify'
|
import dompurify from 'dompurify'
|
||||||
import linkifyHtml from 'linkify-html'
|
import linkifyHtml from 'linkify-html'
|
||||||
|
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
</NcAppNavigationList>
|
</NcAppNavigationList>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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 { toFeedUrl } from '../../utils/url.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
|
import { toFeedUrl } from '../../utils/url.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AddRss',
|
name: 'AddRss',
|
||||||
@ -23,6 +24,7 @@ export default {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
t,
|
||||||
toFeedUrl,
|
toFeedUrl,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<NcActionButton
|
<NcActionButton
|
||||||
v-if="!getSubscriptions.includes(feed.link)"
|
v-if="!getSubByUrl(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,18 +34,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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 { debounce } from '../../utils/debounce.js'
|
import { formatLocaleDate } from '../../utils/time.ts'
|
||||||
import { formatLocaleDate } from '../../utils/time.js'
|
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import { showError } from '../../utils/toast.js'
|
import { showError } from '../../utils/toast.ts'
|
||||||
import { toFeedUrl } from '../../utils/url.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
import { useSubscriptions } from '../../store/subscriptions.js'
|
import { toFeedUrl } from '../../utils/url.ts'
|
||||||
|
import { useSubscriptions } from '../../store/subscriptions.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Search',
|
name: 'Search',
|
||||||
@ -63,22 +64,27 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
feeds: [],
|
feeds: [] as PodcastDataInterface[],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
timeout: null as NodeJS.Timeout | null,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useSubscriptions, ['getSubscriptions']),
|
...mapState(useSubscriptions, ['getSubByUrl']),
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
value() {
|
value() {
|
||||||
this.search()
|
if (this.timeout) {
|
||||||
|
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) {
|
async addSubscription(url: string) {
|
||||||
try {
|
try {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
generateUrl('/apps/gpoddersync/subscription_change/create'),
|
generateUrl('/apps/gpoddersync/subscription_change/create'),
|
||||||
@ -91,14 +97,13 @@ 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()
|
||||||
},
|
},
|
||||||
search: debounce(async function value() {
|
async search() {
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
const currentSearch = this.value
|
const currentSearch = this.value
|
||||||
const feeds = await axios.get(
|
const feeds = await axios.get<PodcastDataInterface[]>(
|
||||||
generateUrl('/apps/repod/search?q={value}', {
|
generateUrl('/apps/repod/search?q={value}', {
|
||||||
value: currentSearch,
|
value: currentSearch,
|
||||||
}),
|
}),
|
||||||
@ -116,7 +121,7 @@ export default {
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 200),
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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.js'
|
import { showError } from '../../utils/toast.ts'
|
||||||
import { toFeedUrl } from '../../utils/url.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
|
import { toFeedUrl } from '../../utils/url.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Toplist',
|
name: 'Toplist',
|
||||||
@ -32,7 +34,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
loading: true,
|
loading: true,
|
||||||
tops: [],
|
tops: [] as PodcastDataInterface[],
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
title() {
|
title() {
|
||||||
@ -49,8 +51,8 @@ export default {
|
|||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
const tops = await axios.get(
|
const tops = await axios.get<PodcastDataInterface[]>(
|
||||||
generateUrl(`/apps/repod/toplist/${this.type}`),
|
generateUrl('/apps/repod/toplist/{type}', { type: this.type }),
|
||||||
)
|
)
|
||||||
this.tops = tops.data
|
this.tops = tops.data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img class="background" :src="imageUrl" />
|
<img class="background" :src="feed.imageUrl" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div>
|
<div>
|
||||||
<NcAvatar
|
<NcAvatar
|
||||||
:display-name="author || title"
|
:display-name="feed.author || feed.title"
|
||||||
:is-no-user="true"
|
:is-no-user="true"
|
||||||
:size="128"
|
:size="128"
|
||||||
:url="imageUrl" />
|
:url="feed.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>{{ title }}</h2>
|
<h2>{{ feed.title }}</h2>
|
||||||
<a :href="link" target="_blank">
|
<a :href="feed.link" target="_blank">
|
||||||
<i>{{ author }}</i>
|
<i>{{ feed.author }}</i>
|
||||||
</a>
|
</a>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<SafeHtml :source="description" />
|
<SafeHtml :source="feed.description || ''" />
|
||||||
</div>
|
</div>
|
||||||
<NcAppNavigationNew
|
<NcAppNavigationNew
|
||||||
v-if="!getSubscriptions.includes(url)"
|
v-if="!getSubByUrl(url)"
|
||||||
:text="t('repod', 'Subscribe')"
|
:text="t('repod', 'Subscribe')"
|
||||||
@click="addSubscription">
|
@click="addSubscription">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@ -35,17 +35,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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.js'
|
import { showError, showSuccess } from '../../utils/toast.ts'
|
||||||
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.js'
|
import { decodeUrl } from '../../utils/url.ts'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import { useSubscriptions } from '../../store/subscriptions.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
|
import { useSubscriptions } from '../../store/subscriptions.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Banner',
|
name: 'Banner',
|
||||||
@ -57,35 +59,20 @@ export default {
|
|||||||
SafeHtml,
|
SafeHtml,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
author: {
|
feed: {
|
||||||
type: String,
|
type: Object as () => PodcastDataInterface,
|
||||||
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, ['getSubscriptions']),
|
...mapState(useSubscriptions, ['getSubByUrl']),
|
||||||
url() {
|
url() {
|
||||||
return decodeUrl(this.$route.params.url)
|
return decodeUrl(this.$route.params.url as string)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useSubscriptions, ['fetch']),
|
...mapActions(useSubscriptions, ['fetch']),
|
||||||
|
t,
|
||||||
async addSubscription() {
|
async addSubscription() {
|
||||||
try {
|
try {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
@ -99,7 +86,6 @@ 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() {
|
||||||
@ -143,11 +129,13 @@ 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) {
|
||||||
|
@ -2,7 +2,11 @@
|
|||||||
<NcListItem
|
<NcListItem
|
||||||
:active="isCurrentEpisode(episode)"
|
:active="isCurrentEpisode(episode)"
|
||||||
class="episode"
|
class="episode"
|
||||||
:details="!oneLine ? formatLocaleDate(new Date(episode.pubDate?.date)) : ''"
|
:details="
|
||||||
|
!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"
|
||||||
@ -78,7 +82,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #indicator>
|
<template #indicator>
|
||||||
<NcProgressBar
|
<NcProgressBar
|
||||||
v-if="isListening(episode) && !oneLine"
|
v-if="episode.action && 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>
|
||||||
@ -88,7 +92,7 @@
|
|||||||
</NcListItem>
|
</NcListItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
NcActionButton,
|
NcActionButton,
|
||||||
NcActionLink,
|
NcActionLink,
|
||||||
@ -102,10 +106,11 @@ import {
|
|||||||
durationToSeconds,
|
durationToSeconds,
|
||||||
formatEpisodeTimestamp,
|
formatEpisodeTimestamp,
|
||||||
formatLocaleDate,
|
formatLocaleDate,
|
||||||
} from '../../utils/time.js'
|
} from '../../utils/time.ts'
|
||||||
import { hasEnded, isListening } from '../../utils/status.js'
|
import { hasEnded, isListening } from '../../utils/status.ts'
|
||||||
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'
|
||||||
@ -113,10 +118,11 @@ 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.js'
|
import { filenameFromUrl } from '../../utils/url.ts'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import { showError } from '../../utils/toast.js'
|
import { showError } from '../../utils/toast.ts'
|
||||||
import { usePlayer } from '../../store/player.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Episode',
|
name: 'Episode',
|
||||||
@ -138,7 +144,7 @@ export default {
|
|||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
episode: {
|
episode: {
|
||||||
type: Object,
|
type: Object as () => EpisodeInterface,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
oneLine: {
|
oneLine: {
|
||||||
@ -152,21 +158,22 @@ export default {
|
|||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
modalEpisode: null,
|
modalEpisode: null as EpisodeInterface | 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,
|
||||||
filenameFromUrl,
|
t,
|
||||||
isCurrentEpisode(episode) {
|
isCurrentEpisode(episode: EpisodeInterface) {
|
||||||
return this.playerEpisode?.url === episode.url
|
return this.playerEpisode?.url === episode.url
|
||||||
},
|
},
|
||||||
async markAs(episode, read) {
|
async markAs(episode: EpisodeInterface, read: boolean) {
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
episode.action = {
|
episode.action = {
|
||||||
@ -176,8 +183,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'),
|
||||||
|
@ -11,17 +11,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { hasEnded, isListening } from '../../utils/status.js'
|
import { hasEnded, isListening } from '../../utils/status.ts'
|
||||||
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.js'
|
import { decodeUrl } from '../../utils/url.ts'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
import { showError } from '../../utils/toast.js'
|
import { showError } from '../../utils/toast.ts'
|
||||||
import { usePlayer } from '../../store/player.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
import { useSettings } from '../../store/settings.js'
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
import { useSettings } from '../../store/settings.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Episodes',
|
name: 'Episodes',
|
||||||
@ -30,7 +32,7 @@ export default {
|
|||||||
Loading,
|
Loading,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
episodes: [],
|
episodes: [] as EpisodeInterface[],
|
||||||
loading: true,
|
loading: true,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
@ -41,27 +43,24 @@ 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)
|
return decodeUrl(this.$route.params.url as string)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -69,13 +68,15 @@ export default {
|
|||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
const episodes = await axios.get(
|
const episodes = await axios.get<EpisodeInterface[]>(
|
||||||
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) => new Date(b.pubDate?.date) - new Date(a.pubDate?.date),
|
(a, b) =>
|
||||||
|
new Date(b.pubDate?.date || '').getTime() -
|
||||||
|
new Date(a.pubDate?.date || '').getTime(),
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<NcGuestContent class="guest">
|
<NcGuestContent class="guest">
|
||||||
<Loading v-if="!currentFavoriteData" />
|
<Loading v-if="!feed.data" />
|
||||||
<NcAvatar
|
<NcAvatar
|
||||||
v-if="currentFavoriteData"
|
v-if="feed.data"
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:display-name="currentFavoriteData.author || currentFavoriteData.title"
|
:display-name="feed.data.author || feed.data.title"
|
||||||
:is-no-user="true"
|
:is-no-user="true"
|
||||||
:size="222"
|
:size="222"
|
||||||
:url="currentFavoriteData.imageUrl" />
|
:url="feed.data.imageUrl" />
|
||||||
<div class="list">
|
<div v-if="feed.data" class="list">
|
||||||
<h2 class="title">{{ currentFavoriteData.title }}</h2>
|
<h2 class="title">{{ feed.data.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="url" />
|
:url="feed.metrics.url" />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</NcGuestContent>
|
</NcGuestContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
|
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.js'
|
import { hasEnded } from '../../utils/status.ts'
|
||||||
import { mapState } from 'pinia'
|
import { showError } from '../../utils/toast.ts'
|
||||||
import { showError } from '../../utils/toast.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
import { useSubscriptions } from '../../store/subscriptions.js'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Favorite',
|
name: 'Favorite',
|
||||||
@ -43,32 +43,28 @@ export default {
|
|||||||
NcGuestContent,
|
NcGuestContent,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
url: {
|
feed: {
|
||||||
type: String,
|
type: Object as () => SubscriptionInterface,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
episodes: [],
|
episodes: [] as EpisodeInterface[],
|
||||||
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(
|
const episodes = await axios.get<EpisodeInterface[]>(
|
||||||
generateUrl('/apps/repod/episodes/list?url={url}', {
|
generateUrl('/apps/repod/episodes/list?url={url}', {
|
||||||
url: this.url,
|
url: this.feed.metrics.url,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
this.episodes = [...episodes.data]
|
this.episodes = [...episodes.data]
|
||||||
.sort(
|
.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(),
|
||||||
)
|
)
|
||||||
.filter((episode) => !this.hasEnded(episode))
|
.filter((episode) => !this.hasEnded(episode))
|
||||||
.slice(0, 4)
|
.slice(0, 4)
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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.js'
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Bar',
|
name: 'Bar',
|
||||||
|
@ -5,11 +5,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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.js'
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Controls',
|
name: 'Controls',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="root">
|
<div v-if="episode && podcastUrl" 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>
|
<script lang="ts">
|
||||||
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.js'
|
import { toFeedUrl } from '../../utils/url.ts'
|
||||||
import { usePlayer } from '../../store/player.js'
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Infos',
|
name: 'Infos',
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
<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="(event) => seek(event.target.value)" />
|
@change="
|
||||||
|
(event) => seek(parseInt((event.target as HTMLInputElement).value))
|
||||||
|
" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { mapActions, mapState } from 'pinia'
|
import { mapActions, mapState } from 'pinia'
|
||||||
import { usePlayer } from '../../store/player.js'
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProgressBar',
|
name: 'ProgressBar',
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="root">
|
<div v-if="currentTime && duration" 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>
|
<script lang="ts">
|
||||||
import { formatTimer } from '../../utils/time.js'
|
import { formatTimer } from '../../utils/time.ts'
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
import { usePlayer } from '../../store/player.js'
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Timer',
|
name: 'Timer',
|
||||||
|
@ -26,17 +26,20 @@
|
|||||||
step="0.1"
|
step="0.1"
|
||||||
type="range"
|
type="range"
|
||||||
:value="volume"
|
:value="volume"
|
||||||
@change="(event) => setVolume(event.target.value)" />
|
@change="
|
||||||
|
(event) =>
|
||||||
|
setVolume(parseInt((event.target as HTMLInputElement).value))
|
||||||
|
" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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.js'
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Volume',
|
name: 'Volume',
|
||||||
|
@ -8,10 +8,11 @@
|
|||||||
</NcAppNavigationItem>
|
</NcAppNavigationItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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',
|
||||||
@ -21,6 +22,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
generateUrl,
|
generateUrl,
|
||||||
|
t,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -39,12 +39,13 @@
|
|||||||
</NcAppNavigationItem>
|
</NcAppNavigationItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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 { useSettings } from '../../store/settings.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
|
import { useSettings } from '../../store/settings.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Filters',
|
name: 'Filters',
|
||||||
@ -66,6 +67,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useSettings, ['setFilters']),
|
...mapActions(useSettings, ['setFilters']),
|
||||||
|
t,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -29,12 +29,13 @@
|
|||||||
</NcAppNavigationItem>
|
</NcAppNavigationItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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',
|
||||||
@ -50,11 +51,13 @@ export default {
|
|||||||
}),
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
generateUrl,
|
generateUrl,
|
||||||
async importOpml(event) {
|
t,
|
||||||
|
async importOpml(event: Event) {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData(event.target)
|
const target = event.target as HTMLFormElement
|
||||||
this.importLoading = true
|
const formData = new FormData(target)
|
||||||
await axios.post(event.target.action, formData)
|
this.loading = true
|
||||||
|
await axios.post(target.action, formData)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -8,9 +8,10 @@
|
|||||||
</NcAppNavigationItem>
|
</NcAppNavigationItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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',
|
||||||
@ -18,5 +19,8 @@ export default {
|
|||||||
NcAppNavigationItem,
|
NcAppNavigationItem,
|
||||||
StarIcon,
|
StarIcon,
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
t,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</NcAppNavigationSettings>
|
</NcAppNavigationSettings>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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'
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</NcAppNavigationItem>
|
</NcAppNavigationItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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,7 +23,8 @@ 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 { usePlayer } from '../../store/player.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
|
import { usePlayer } from '../../store/player.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Speed',
|
name: 'Speed',
|
||||||
@ -41,8 +42,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(usePlayer, ['setRate']),
|
...mapActions(usePlayer, ['setRate']),
|
||||||
changeRate(diff) {
|
t,
|
||||||
const newRate = (this.rate + diff).toPrecision(2)
|
changeRate(diff: number) {
|
||||||
|
const newRate = parseFloat((this.rate + diff).toPrecision(2))
|
||||||
this.setRate(newRate > 0 ? newRate : this.rate)
|
this.setRate(newRate > 0 ? newRate : this.rate)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<NcAppNavigationItem
|
<NcAppNavigationItem
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:name="feed ? feed.title : url"
|
:name="feed?.data?.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="isFavorite"
|
:model-value="feed?.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="!isFavorite" :size="20" />
|
<StarPlusIcon v-if="!feed?.isFavorite" :size="20" />
|
||||||
<StarRemoveIcon v-if="isFavorite" :size="20" />
|
<StarRemoveIcon v-if="feed?.isFavorite" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
<NcActionButton
|
<NcActionButton
|
||||||
@ -27,29 +27,30 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NcAvatar
|
<NcAvatar
|
||||||
v-if="feed"
|
:display-name="feed?.data?.author || feed?.data?.title"
|
||||||
:display-name="feed.author || feed.title"
|
|
||||||
:is-no-user="true"
|
:is-no-user="true"
|
||||||
:url="feed.imageUrl" />
|
:url="feed?.data?.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>
|
<script lang="ts">
|
||||||
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.js'
|
import { showError } from '../../utils/toast.ts'
|
||||||
import { toFeedUrl } from '../../utils/url.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
import { useSubscriptions } from '../../store/subscriptions.js'
|
import { toFeedUrl } from '../../utils/url.ts'
|
||||||
|
import { useSubscriptions } from '../../store/subscriptions.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Subscription',
|
name: 'Subscription',
|
||||||
@ -72,17 +73,17 @@ export default {
|
|||||||
data: () => ({
|
data: () => ({
|
||||||
failed: false,
|
failed: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
feed: null,
|
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useSubscriptions, ['getFavorites']),
|
...mapState(useSubscriptions, ['subs']),
|
||||||
isFavorite() {
|
feed() {
|
||||||
return this.getFavorites.map((fav) => fav.url).includes(this.url)
|
return this.subs.find((sub) => sub.metrics.url === this.url)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
const podcastData = await axios.get(
|
const podcastData =
|
||||||
|
await axios.get<PersonalSettingsPodcastDataInterface>(
|
||||||
generateUrl(
|
generateUrl(
|
||||||
'/apps/gpoddersync/personal_settings/podcast_data?url={url}',
|
'/apps/gpoddersync/personal_settings/podcast_data?url={url}',
|
||||||
{
|
{
|
||||||
@ -90,8 +91,7 @@ export default {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
this.feed = podcastData.data.data
|
this.addMetadatas(this.url, 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,12 +100,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useSubscriptions, [
|
...mapActions(useSubscriptions, ['fetch', 'addMetadatas', 'setFavorite']),
|
||||||
'fetch',
|
t,
|
||||||
'addFavorite',
|
|
||||||
'editFavoriteData',
|
|
||||||
'removeFavorite',
|
|
||||||
]),
|
|
||||||
toFeedUrl,
|
toFeedUrl,
|
||||||
async deleteSubscription() {
|
async deleteSubscription() {
|
||||||
if (
|
if (
|
||||||
@ -123,23 +119,20 @@ export default {
|
|||||||
console.error(e)
|
console.error(e)
|
||||||
showError(t('repod', 'Error while removing the feed'))
|
showError(t('repod', 'Error while removing the feed'))
|
||||||
} finally {
|
} finally {
|
||||||
this.removeFavorite(this.url)
|
this.setFavorite(this.url, false)
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.fetch()
|
this.fetch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
switchFavorite(value) {
|
switchFavorite(value: boolean) {
|
||||||
if (value) {
|
if (value) {
|
||||||
if (this.getFavorites.length >= 10) {
|
if (this.subs.filter((sub) => sub.isFavorite).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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -12,16 +12,13 @@
|
|||||||
<Loading v-if="loading" />
|
<Loading v-if="loading" />
|
||||||
<NcAppNavigationList v-if="!loading">
|
<NcAppNavigationList v-if="!loading">
|
||||||
<Subscription
|
<Subscription
|
||||||
v-for="url of getFavorites.map((fav) => fav.url)"
|
v-for="sub of subs.filter((sub) => sub.isFavorite)"
|
||||||
:key="url"
|
:key="sub.metrics.url"
|
||||||
:url="url" />
|
:url="sub.metrics.url" />
|
||||||
<Subscription
|
<Subscription
|
||||||
v-for="url of getSubscriptions.filter(
|
v-for="sub of subs.filter((sub) => !sub.isFavorite)"
|
||||||
(sub) =>
|
:key="sub.metrics.url"
|
||||||
!getFavorites.map((fav) => fav.url).includes(sub),
|
:url="sub.metrics.url" />
|
||||||
)"
|
|
||||||
:key="url"
|
|
||||||
:url="url" />
|
|
||||||
</NcAppNavigationList>
|
</NcAppNavigationList>
|
||||||
</NcAppContentList>
|
</NcAppContentList>
|
||||||
</template>
|
</template>
|
||||||
@ -31,7 +28,7 @@
|
|||||||
</AppNavigation>
|
</AppNavigation>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
NcAppContentList,
|
NcAppContentList,
|
||||||
NcAppNavigationList,
|
NcAppNavigationList,
|
||||||
@ -43,8 +40,9 @@ 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.js'
|
import { showError } from '../../utils/toast.ts'
|
||||||
import { useSubscriptions } from '../../store/subscriptions.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
|
import { useSubscriptions } from '../../store/subscriptions.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Subscriptions',
|
name: 'Subscriptions',
|
||||||
@ -62,7 +60,7 @@ export default {
|
|||||||
loading: true,
|
loading: true,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useSubscriptions, ['getSubscriptions', 'getFavorites']),
|
...mapState(useSubscriptions, ['subs']),
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
@ -76,6 +74,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useSubscriptions, ['fetch']),
|
...mapActions(useSubscriptions, ['fetch']),
|
||||||
|
t,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
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.js'
|
import router from './router.ts'
|
||||||
|
|
||||||
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')
|
@ -1,18 +1,19 @@
|
|||||||
|
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.js'
|
import { formatEpisodeTimestamp } from '../utils/time.ts'
|
||||||
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,
|
currentTime: null as number | null,
|
||||||
duration: null,
|
duration: null as number | null,
|
||||||
episode: null,
|
episode: null as EpisodeInterface | null,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
paused: null,
|
paused: true,
|
||||||
podcastUrl: null,
|
podcastUrl: null as string | null,
|
||||||
volume: 1,
|
volume: 1,
|
||||||
rate: 1,
|
rate: 1,
|
||||||
started: 0,
|
started: 0,
|
||||||
@ -29,16 +30,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, podcastUrl) {
|
async load(episode: EpisodeInterface | null, podcastUrl?: string) {
|
||||||
this.episode = episode
|
this.episode = episode
|
||||||
this.podcastUrl = podcastUrl
|
this.podcastUrl = podcastUrl || null
|
||||||
|
|
||||||
if (this.episode) {
|
if (this.episode?.url) {
|
||||||
audio.src = this.episode.url
|
audio.src = this.episode.url
|
||||||
audio.load()
|
audio.load()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const action = await axios.get(
|
const action = await axios.get<EpisodeActionInterface>(
|
||||||
generateUrl('/apps/repod/episodes/action?url={url}', {
|
generateUrl('/apps/repod/episodes/action?url={url}', {
|
||||||
url: this.episode.url,
|
url: this.episode.url,
|
||||||
}),
|
}),
|
||||||
@ -72,7 +73,7 @@ export const usePlayer = defineStore('player', {
|
|||||||
this.paused = false
|
this.paused = false
|
||||||
this.started = audio.currentTime
|
this.started = audio.currentTime
|
||||||
},
|
},
|
||||||
seek(currentTime) {
|
seek(currentTime: number) {
|
||||||
audio.currentTime = currentTime
|
audio.currentTime = currentTime
|
||||||
this.time()
|
this.time()
|
||||||
},
|
},
|
||||||
@ -81,6 +82,10 @@ 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,
|
||||||
@ -91,14 +96,15 @@ 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) {
|
setVolume(volume: number) {
|
||||||
audio.volume = volume
|
audio.volume = volume
|
||||||
},
|
},
|
||||||
setRate(rate) {
|
setRate(rate: number) {
|
||||||
audio.playbackRate = rate
|
audio.playbackRate = rate
|
||||||
},
|
},
|
||||||
},
|
},
|
@ -1,10 +1,16 @@
|
|||||||
import { getCookie, setCookie } from '../utils/cookies.js'
|
import { getCookie, setCookie } from '../utils/cookies.ts'
|
||||||
|
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,
|
||||||
@ -23,7 +29,7 @@ export const useSettings = defineStore('settings', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setFilters(filters) {
|
setFilters(filters: Partial<FiltersInterface>) {
|
||||||
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)
|
||||||
},
|
},
|
@ -1,58 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
59
src/store/subscriptions.ts
Normal file
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
|
* @param {string} name Nom du cookie à récupérer
|
||||||
* @return {string|null}
|
* @return {string|null}
|
||||||
*/
|
*/
|
||||||
export const getCookie = (name) => {
|
export const getCookie = (name: string): string | null => {
|
||||||
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) => {
|
|||||||
* @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, value, days) => {
|
export const setCookie = (name: string, value: string, days: number) => {
|
||||||
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;`
|
@ -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
|
// 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))
|
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024))
|
||||||
return (
|
return (
|
||||||
(size / Math.pow(1024, i)).toFixed(2) * 1 +
|
(size / Math.pow(1024, i)).toFixed(2) +
|
||||||
' ' +
|
' ' +
|
||||||
['B', 'kB', 'MB', 'GB', 'TB'][i]
|
['B', 'kB', 'MB', 'GB', 'TB'][i]
|
||||||
)
|
)
|
@ -1,4 +1,6 @@
|
|||||||
export const hasEnded = (episode) =>
|
import type { EpisodeInterface } from './types'
|
||||||
|
|
||||||
|
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' ||
|
||||||
@ -6,7 +8,7 @@ export const hasEnded = (episode) =>
|
|||||||
episode.action.total > 0 &&
|
episode.action.total > 0 &&
|
||||||
episode.action.position >= episode.action.total))
|
episode.action.position >= episode.action.total))
|
||||||
|
|
||||||
export const isListening = (episode) =>
|
export const isListening = (episode: EpisodeInterface) =>
|
||||||
episode.action &&
|
episode.action &&
|
||||||
episode.action.action &&
|
episode.action.action &&
|
||||||
episode.action.action.toLowerCase() === 'play' &&
|
episode.action.action.toLowerCase() === 'play' &&
|
@ -3,9 +3,9 @@
|
|||||||
* @param {Date} date The date
|
* @param {Date} date The date
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
export const formatTimer = (date) => {
|
export const formatTimer = (date: Date): string => {
|
||||||
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) => {
|
|||||||
* @param {Date} date The date
|
* @param {Date} date The date
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
export const formatEpisodeTimestamp = (date) => {
|
export const formatEpisodeTimestamp = (date: Date): string => {
|
||||||
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) => {
|
|||||||
* @param {Date} date The date
|
* @param {Date} date The date
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
export const formatLocaleDate = (date) =>
|
export const formatLocaleDate = (date: Date): string =>
|
||||||
date.toLocaleDateString(undefined, { dateStyle: 'medium' })
|
date.toLocaleDateString(undefined, { dateStyle: 'medium' })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,7 +44,7 @@ export const formatLocaleDate = (date) =>
|
|||||||
* @param {string} duration The duration feed's entry
|
* @param {string} duration The duration feed's entry
|
||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
export const durationToSeconds = (duration) => {
|
export const durationToSeconds = (duration: string): number => {
|
||||||
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
|
@ -1,12 +0,0 @@
|
|||||||
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)')
|
|
14
src/utils/toast.ts
Normal file
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
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,4 +0,0 @@
|
|||||||
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()
|
|
5
src/utils/url.ts
Normal file
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()
|
@ -12,13 +12,14 @@
|
|||||||
</AppContent>
|
</AppContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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',
|
||||||
@ -33,6 +34,9 @@ export default {
|
|||||||
data: () => ({
|
data: () => ({
|
||||||
search: '',
|
search: '',
|
||||||
}),
|
}),
|
||||||
|
methods: {
|
||||||
|
t,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -6,27 +6,23 @@
|
|||||||
<Alert />
|
<Alert />
|
||||||
</template>
|
</template>
|
||||||
</EmptyContent>
|
</EmptyContent>
|
||||||
<Banner
|
<Banner v-if="feed" :feed="feed" />
|
||||||
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>
|
<script lang="ts">
|
||||||
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.js'
|
import { decodeUrl } from '../utils/url.ts'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
|
import { t } from '@nextcloud/l10n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Feed',
|
name: 'Feed',
|
||||||
@ -41,16 +37,17 @@ export default {
|
|||||||
data: () => ({
|
data: () => ({
|
||||||
failed: false,
|
failed: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
feed: null,
|
feed: null as PodcastDataInterface | null,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
url() {
|
url() {
|
||||||
return decodeUrl(this.$route.params.url)
|
return decodeUrl(this.$route.params.url as string)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
const podcastData = await axios.get(
|
this.loading = true
|
||||||
|
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
|
||||||
@ -61,5 +58,8 @@ export default {
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
t,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -13,12 +13,13 @@
|
|||||||
</AppContent>
|
</AppContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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',
|
||||||
@ -33,5 +34,8 @@ export default {
|
|||||||
return generateUrl('/settings/apps/installed/gpoddersync')
|
return generateUrl('/settings/apps/installed/gpoddersync')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
t,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppContent>
|
<AppContent>
|
||||||
<EmptyContent
|
<EmptyContent
|
||||||
v-if="!getFavorites.length"
|
v-if="!favorites.length"
|
||||||
:description="
|
:description="
|
||||||
t('repod', 'Pin some subscriptions to see their latest updates')
|
t('repod', 'Pin some subscriptions to see their latest updates')
|
||||||
"
|
"
|
||||||
@ -10,21 +10,22 @@
|
|||||||
<StarOffIcon />
|
<StarOffIcon />
|
||||||
</template>
|
</template>
|
||||||
</EmptyContent>
|
</EmptyContent>
|
||||||
<ul v-if="getFavorites.length">
|
<ul v-if="favorites.length">
|
||||||
<li v-for="url in getFavorites.map((fav) => fav.url)" :key="url">
|
<li v-for="favorite in favorites" :key="favorite.metrics.url">
|
||||||
<Favorite :url="url" />
|
<Favorite :feed="favorite" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
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 { useSubscriptions } from '../store/subscriptions.js'
|
import { t } from '@nextcloud/l10n'
|
||||||
|
import { useSubscriptions } from '../store/subscriptions.ts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
@ -35,7 +36,13 @@ export default {
|
|||||||
StarOffIcon,
|
StarOffIcon,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useSubscriptions, ['getFavorites']),
|
...mapState(useSubscriptions, ['subs']),
|
||||||
|
favorites() {
|
||||||
|
return this.subs.filter((sub) => sub.isFavorite)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
t,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
19
tsconfig.json
Normal file
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,
|
||||||
|
},
|
||||||
|
}
|
@ -4,11 +4,6 @@ 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: {
|
||||||
@ -19,7 +14,7 @@ const config = defineConfig(({ mode }) => ({
|
|||||||
|
|
||||||
export default createAppConfig(
|
export default createAppConfig(
|
||||||
{
|
{
|
||||||
main: 'src/main.js',
|
main: 'src/main.ts',
|
||||||
},
|
},
|
||||||
{ config, inlineCSS: true },
|
{ config, inlineCSS: true },
|
||||||
)
|
)
|
Loading…
Reference in New Issue
Block a user