typescript #149 #152

Merged
Xefir merged 8 commits from typescript into main 2024-09-14 15:26:18 +00:00
18 changed files with 197 additions and 156 deletions
Showing only changes of commit 83e3358e9b - Show all commits

View File

@ -1,6 +1,7 @@
module.exports = { module.exports = {
extends: [ extends: [
'@nextcloud', '@nextcloud',
'@vue/eslint-config-typescript',
'plugin:pinia/recommended', 'plugin:pinia/recommended',
'plugin:prettier/recommended', 'plugin:prettier/recommended',
], ],

43
package-lock.json generated
View File

@ -18,11 +18,11 @@
"petite-utils": "^0.0.5-3", "petite-utils": "^0.0.5-3",
"pinia": "^2.2.2", "pinia": "^2.2.2",
"toastify-js": "^1.12.0", "toastify-js": "^1.12.0",
"vite": "^5.4.4", "vite": "^5.4.5",
"vite-plugin-vue-devtools": "^7.4.5", "vite-plugin-vue-devtools": "^7.4.5",
"vue": "^3.5.4", "vue": "^3.5.4",
"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",
@ -30,12 +30,13 @@
"@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", "@types/toastify-js": "^1.12.3",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1", "@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", "ts-node": "^10.9.2",
"typescript": "^5.6.2", "typescript": "5.5.4",
"vue-eslint-parser": "^9.4.3", "vue-eslint-parser": "^9.4.3",
"vue-tsc": "^2.1.6" "vue-tsc": "^2.1.6"
} }
@ -1033,7 +1034,6 @@
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
"integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"eslint-visitor-keys": "^3.3.0" "eslint-visitor-keys": "^3.3.0"
}, },
@ -1049,7 +1049,6 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}, },
@ -1062,7 +1061,6 @@
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
"integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0" "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
} }
@ -2314,7 +2312,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
"integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/scope-manager": "7.18.0",
@ -2348,7 +2345,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.18.0", "@typescript-eslint/types": "7.18.0",
@ -2377,7 +2373,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
"integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.18.0", "@typescript-eslint/types": "7.18.0",
"@typescript-eslint/visitor-keys": "7.18.0" "@typescript-eslint/visitor-keys": "7.18.0"
@ -2395,7 +2390,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
"integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0",
"@typescript-eslint/utils": "7.18.0", "@typescript-eslint/utils": "7.18.0",
@ -2423,7 +2417,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
"integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
}, },
@ -2437,7 +2430,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
"integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.18.0", "@typescript-eslint/types": "7.18.0",
"@typescript-eslint/visitor-keys": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0",
@ -2466,7 +2458,6 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
@ -2479,7 +2470,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
"integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/scope-manager": "7.18.0",
@ -2502,7 +2492,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
"integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.18.0", "@typescript-eslint/types": "7.18.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
@ -2520,7 +2509,6 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}, },
@ -2740,7 +2728,6 @@
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz", "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz",
"integrity": "sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==", "integrity": "sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1", "@typescript-eslint/parser": "^7.1.1",
@ -3113,7 +3100,6 @@
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -4415,9 +4401,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.20", "version": "1.5.22",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.20.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.22.tgz",
"integrity": "sha512-74mdl6Fs1HHzK9SUX4CKFxAtAe3nUns48y79TskHNAG6fGOlLfyKA4j855x+0b5u8rWJIrlaG9tcTPstMlwjIw==" "integrity": "sha512-tKYm5YHPU1djz0O+CGJ+oJIvimtsCcwR2Z9w7Skh08lUdyzXY5djods3q+z2JkWdb7tCcmM//eVavSRAiaPRNg=="
}, },
"node_modules/elliptic": { "node_modules/elliptic": {
"version": "6.5.7", "version": "6.5.7",
@ -6157,7 +6143,6 @@
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"array-union": "^2.1.0", "array-union": "^2.1.0",
"dir-glob": "^3.0.1", "dir-glob": "^3.0.1",
@ -6200,8 +6185,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true, "dev": true
"peer": true
}, },
"node_modules/has-bigints": { "node_modules/has-bigints": {
"version": "1.0.2", "version": "1.0.2",
@ -8347,8 +8331,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true, "dev": true
"peer": true
}, },
"node_modules/node-gettext": { "node_modules/node-gettext": {
"version": "3.0.0", "version": "3.0.0",
@ -9818,7 +9801,6 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -10803,7 +10785,6 @@
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
"integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=16" "node": ">=16"
}, },
@ -10995,9 +10976,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.6.2", "version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"

View File

@ -27,11 +27,11 @@
"petite-utils": "^0.0.5-3", "petite-utils": "^0.0.5-3",
"pinia": "^2.2.2", "pinia": "^2.2.2",
"toastify-js": "^1.12.0", "toastify-js": "^1.12.0",
"vite": "^5.4.4", "vite": "^5.4.5",
"vite-plugin-vue-devtools": "^7.4.5", "vite-plugin-vue-devtools": "^7.4.5",
"vue": "^3.5.4", "vue": "^3.5.4",
"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",
@ -39,12 +39,13 @@
"@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", "@types/toastify-js": "^1.12.3",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1", "@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", "ts-node": "^10.9.2",
"typescript": "^5.6.2", "typescript": "5.5.4",
"vue-eslint-parser": "^9.4.3", "vue-eslint-parser": "^9.4.3",
"vue-tsc": "^2.1.6" "vue-tsc": "^2.1.6"
} }

View File

@ -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')"
@ -39,6 +39,7 @@ 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 'petite-utils' import { debounce } from 'petite-utils'
import { formatLocaleDate } from '../../utils/time.ts' import { formatLocaleDate } from '../../utils/time.ts'
@ -64,15 +65,38 @@ export default {
}, },
}, },
data: () => ({ data: () => ({
feeds: [], feeds: [] as PodcastDataInterface[],
loading: false, loading: false,
}), }),
computed: { computed: {
...mapState(useSubscriptions, ['getSubscriptions']), ...mapState(useSubscriptions, ['getSubByUrl']),
}, },
watch: { watch: {
value() { value() {
this.search() const that = this
debounce(async function () {
try {
that.loading = true
const currentSearch = that.value
const feeds = await axios.get<PodcastDataInterface[]>(
generateUrl('/apps/repod/search?q={value}', {
value: currentSearch,
}),
)
if (currentSearch === that.value) {
that.feeds = [...feeds.data].sort(
(a, b) => b.fetchedAtUnix - a.fetchedAtUnix,
)
}
} catch (e) {
console.error(e)
showError(t('repod', 'Could not fetch search results'))
} finally {
if (that.feeds) {
that.loading = false
}
}
}, 200)
}, },
}, },
methods: { methods: {
@ -80,7 +104,7 @@ export default {
formatLocaleDate, formatLocaleDate,
t, t,
toFeedUrl, toFeedUrl,
addSubscription: async (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'),
@ -96,29 +120,6 @@ export default {
this.fetch() this.fetch()
}, },
search: debounce(async function value() {
try {
this.loading = true
const currentSearch = this.value
const feeds = await axios.get(
generateUrl('/apps/repod/search?q={value}', {
value: currentSearch,
}),
)
if (currentSearch === this.value) {
this.feeds = [...feeds.data].sort(
(a, b) => b.fetchedAtUnix - a.fetchedAtUnix,
)
}
} catch (e) {
console.error(e)
showError(t('repod', 'Could not fetch search results'))
} finally {
if (this.feeds) {
this.loading = false
}
}
}, 200),
}, },
} }
</script> </script>

View File

@ -12,11 +12,13 @@
</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.ts' import { showError } from '../../utils/toast.ts'
import { t } from '@nextcloud/l10n'
import { toFeedUrl } from '../../utils/url.ts' import { toFeedUrl } from '../../utils/url.ts'
export default { export default {
@ -32,7 +34,7 @@ export default {
}, },
data: () => ({ data: () => ({
loading: true, loading: true,
tops: [], tops: [] as PodcastDataInterface[],
}), }),
computed: { computed: {
title() { title() {

View File

@ -23,7 +23,7 @@
<SafeHtml :source="description" /> <SafeHtml :source="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,7 +35,7 @@
</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.ts' import { showError, showSuccess } from '../../utils/toast.ts'
@ -45,6 +45,7 @@ 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.ts'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { t } from '@nextcloud/l10n'
import { useSubscriptions } from '../../store/subscriptions.ts' import { useSubscriptions } from '../../store/subscriptions.ts'
export default { export default {
@ -79,9 +80,9 @@ export default {
}, },
}, },
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: {
@ -106,6 +107,7 @@ export default {
window.navigator.clipboard.writeText(this.url) window.navigator.clipboard.writeText(this.url)
showSuccess(t('repod', 'Link copied to the clipboard')) showSuccess(t('repod', 'Link copied to the clipboard'))
}, },
t,
}, },
} }
</script> </script>

View File

@ -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,
@ -106,6 +110,7 @@ import {
import { hasEnded, isListening } from '../../utils/status.ts' 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'
@ -116,6 +121,7 @@ import axios from '@nextcloud/axios'
import { filenameFromUrl } from '../../utils/url.ts' import { filenameFromUrl } from '../../utils/url.ts'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { showError } from '../../utils/toast.ts' import { showError } from '../../utils/toast.ts'
import { t } from '@nextcloud/l10n'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.ts'
export default { export default {
@ -138,7 +144,7 @@ export default {
}, },
props: { props: {
episode: { episode: {
type: Object, type: Object as () => EpisodeInterface,
required: true, required: true,
}, },
oneLine: { oneLine: {
@ -152,7 +158,7 @@ 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' }),
@ -163,10 +169,11 @@ export default {
hasEnded, hasEnded,
isListening, isListening,
filenameFromUrl, filenameFromUrl,
isCurrentEpisode(episode) { t,
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'),

View File

@ -11,15 +11,17 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { hasEnded, isListening } from '../../utils/status.ts' 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.ts' 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.ts' import { showError } from '../../utils/toast.ts'
import { t } from '@nextcloud/l10n'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.ts'
import { useSettings } from '../../store/settings.ts' import { useSettings } from '../../store/settings.ts'
@ -30,7 +32,7 @@ export default {
Loading, Loading,
}, },
data: () => ({ data: () => ({
episodes: [], episodes: [] as EpisodeInterface[],
loading: true, loading: true,
}), }),
computed: { computed: {
@ -54,14 +56,14 @@ export default {
}) })
}, },
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,
) )
} }
}, },
@ -75,7 +77,9 @@ export default {
}), }),
) )
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)

View File

@ -8,7 +8,7 @@
:is-no-user="true" :is-no-user="true"
:size="222" :size="222"
:url="currentFavoriteData.imageUrl" /> :url="currentFavoriteData.imageUrl" />
<div class="list"> <div v-if="currentFavoriteData" class="list">
<h2 class="title">{{ currentFavoriteData.title }}</h2> <h2 class="title">{{ currentFavoriteData.title }}</h2>
<Loading v-if="loading" /> <Loading v-if="loading" />
<ul v-if="!loading"> <ul v-if="!loading">
@ -23,15 +23,17 @@
</NcGuestContent> </NcGuestContent>
</template> </template>
<script> <script lang="ts">
import { NcAvatar, NcGuestContent } from '@nextcloud/vue' import { NcAvatar, NcGuestContent } from '@nextcloud/vue'
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 { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { hasEnded } from '../../utils/status.ts' import { hasEnded } from '../../utils/status.ts'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { showError } from '../../utils/toast.ts' import { showError } from '../../utils/toast.ts'
import { t } from '@nextcloud/l10n'
import { useSubscriptions } from '../../store/subscriptions.ts' import { useSubscriptions } from '../../store/subscriptions.ts'
export default { export default {
@ -49,13 +51,13 @@ export default {
}, },
}, },
data: () => ({ data: () => ({
episodes: [], episodes: [] as EpisodeInterface[],
loading: true, loading: true,
}), }),
computed: { computed: {
...mapState(useSubscriptions, ['getFavorites']), ...mapState(useSubscriptions, ['getSubByUrl']),
currentFavoriteData() { currentFavoriteData() {
return this.getFavorites.find((fav) => fav.url === this.url) return this.getSubByUrl(this.url)?.data
}, },
}, },
async mounted() { async mounted() {
@ -68,7 +70,9 @@ export default {
) )
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)

View File

@ -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'

View File

@ -5,7 +5,7 @@
</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'

View File

@ -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,7 +12,7 @@
</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'

View File

@ -1,14 +1,17 @@
<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.ts' import { usePlayer } from '../../store/player.ts'

View File

@ -1,12 +1,12 @@
<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.ts' import { formatTimer } from '../../utils/time.ts'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { usePlayer } from '../../store/player.ts' import { usePlayer } from '../../store/player.ts'

View File

@ -26,11 +26,14 @@
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'

View File

@ -4,11 +4,8 @@ import { defineStore } from 'pinia'
export const useSettings = defineStore('settings', { export const useSettings = defineStore('settings', {
state: () => { state: () => {
const cookie = getCookie('repod.filters')
if (cookie) {
try { try {
const filters = JSON.parse(cookie) const filters = JSON.parse(getCookie('repod.filters') || '{}') || {}
return { return {
filters: { filters: {
listened: filters.listened, listened: filters.listened,
@ -16,9 +13,7 @@ export const useSettings = defineStore('settings', {
unlistened: filters.unlistened, unlistened: filters.unlistened,
}, },
} }
} catch {} } catch {
}
return { return {
filters: { filters: {
listened: true, listened: true,
@ -26,6 +21,7 @@ export const useSettings = defineStore('settings', {
unlistened: true, unlistened: true,
}, },
} }
}
}, },
actions: { actions: {
setFilters(filters: FiltersInterface) { setFilters(filters: FiltersInterface) {

View File

@ -1,3 +1,8 @@
import type {
PodcastDataInterface,
PodcastMetricsInterface,
SubscriptionInterface,
} from '../utils/types'
import { getCookie, setCookie } from '../utils/cookies' import { getCookie, setCookie } from '../utils/cookies'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
@ -5,52 +10,47 @@ import { generateUrl } from '@nextcloud/router'
export const useSubscriptions = defineStore('subscriptions', { export const useSubscriptions = defineStore('subscriptions', {
state: () => ({ state: () => ({
subs: [], subs: [] as SubscriptionInterface[],
favs: [],
}), }),
getters: { getters: {
getSubscriptions: (state) => { getSubByUrl: (state) => (url: string) =>
return state.subs state.subs.find((sub) => sub.metrics.url === url),
},
getFavorites: (state) => {
return state.favs
.filter((fav) => state.subs.includes(fav.url))
.sort((fav) => fav.lastPub)
},
}, },
actions: { actions: {
async fetch() { async fetch() {
const metrics = await axios.get( let favorites: string[] = []
try {
favorites = JSON.parse(getCookie('repod.favorites') || '[]') || []
} catch {}
const metrics = await axios.get<PodcastMetricsInterface>(
generateUrl('/apps/gpoddersync/personal_settings/metrics'), generateUrl('/apps/gpoddersync/personal_settings/metrics'),
) )
const subs = [...metrics.data.subscriptions].sort( this.subs = [...metrics.data.subscriptions]
(a, b) => b.listenedSeconds - a.listenedSeconds, .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,
}))
},
addFavoriteData(data: PodcastDataInterface) {
this.subs = this.subs.map((sub) =>
sub.metrics.url === data.link ? { ...sub, data } : sub,
)
},
setFavorite(link: string, isFavorite: boolean) {
this.subs.map((sub) =>
sub.metrics.url === link ? { ...sub, isFavorite } : sub,
) )
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( setCookie(
'repod.favorites', 'repod.favorites',
JSON.stringify(this.favs.map((fav) => fav.url)), JSON.stringify(
365, this.subs
) .filter((sub) => sub.isFavorite)
}, .map((sub) => sub.metrics.url),
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, 365,
) )
}, },

View File

@ -12,7 +12,7 @@ export interface EpisodeActionInterface {
export interface EpisodeInterface { export interface EpisodeInterface {
title: string title: string
url?: string url: string
name: string name: string
link?: string link?: string
image?: string image?: string
@ -21,7 +21,11 @@ export interface EpisodeInterface {
guid: string guid: string
type?: string type?: string
size?: number size?: number
pubDate?: Date pubDate?: {
date: string
timezone_type: number
timezone: string
}
duration?: string duration?: string
action?: EpisodeActionInterface action?: EpisodeActionInterface
} }
@ -31,3 +35,35 @@ export interface FiltersInterface {
listening: boolean listening: boolean
unlistened: boolean unlistened: boolean
} }
export interface PodcastDataInterface {
title: string
author?: string
link: string
description?: string
imageUrl?: string
fetchedAtUnix: number
imageBlob?: string | null
}
export interface PodcastMetricsInterface {
subscriptions: [
{
url: string
listenedSeconds: number
actionCounts: {
delete: number
download: number
flattr: number
new: number
play: number
}
},
]
}
export interface SubscriptionInterface {
data?: PodcastDataInterface
isFavorite: boolean
metrics: PodcastMetricsInterface['subscriptions'][0]
}