refactor: 🚧 introducing typescript (not working now)
Some checks failed
repod / xml (push) Successful in 37s
repod / php (push) Successful in 54s
repod / nodejs (push) Failing after 56s
repod / release (push) Has been skipped

This commit is contained in:
Michel Roux 2024-09-13 08:56:04 +02:00
parent c983ab8d3b
commit fdee813bce
47 changed files with 636 additions and 328 deletions

View File

@ -4,6 +4,7 @@ module.exports = {
'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',

581
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "repod", "name": "repod",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"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",
@ -24,10 +24,11 @@
"@nextcloud/vue": "9.0.0-alpha.5", "@nextcloud/vue": "9.0.0-alpha.5",
"dompurify": "^3.1.6", "dompurify": "^3.1.6",
"linkify-html": "^4.1.3", "linkify-html": "^4.1.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.3", "vite": "^5.4.4",
"vite-plugin-vue-devtools": "^7.4.4", "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.4"
@ -37,8 +38,14 @@
"@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/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.6.2",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^2.1.6"
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,13 +29,14 @@
</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 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',
@ -55,6 +56,7 @@ export default {
methods: { methods: {
filenameFromUrl, filenameFromUrl,
humanFileSize, humanFileSize,
t,
}, },
} }
</script> </script>

View File

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

View File

@ -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,
}, },
} }

View File

@ -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 axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { debounce } from '../../utils/debounce.js' import { debounce } from 'petite-utils'
import { formatLocaleDate } from '../../utils/time.js' import { formatLocaleDate } from '../../utils/time.ts'
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',
@ -77,8 +78,9 @@ export default {
methods: { methods: {
...mapActions(useSubscriptions, ['fetch']), ...mapActions(useSubscriptions, ['fetch']),
formatLocaleDate, formatLocaleDate,
t,
toFeedUrl, toFeedUrl,
async addSubscription(url) { addSubscription: async (url) => {
try { try {
await axios.post( await axios.post(
generateUrl('/apps/gpoddersync/subscription_change/create'), generateUrl('/apps/gpoddersync/subscription_change/create'),

View File

@ -16,8 +16,8 @@
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 { showError } from '../../utils/toast.js' import { showError } from '../../utils/toast.ts'
import { toFeedUrl } from '../../utils/url.js' import { toFeedUrl } from '../../utils/url.ts'
export default { export default {
name: 'Toplist', name: 'Toplist',

View File

@ -38,14 +38,14 @@
<script> <script>
import { NcAppNavigationNew, NcAvatar } from '@nextcloud/vue' import { NcAppNavigationNew, NcAvatar } from '@nextcloud/vue'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import { showError, showSuccess } from '../../utils/toast.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 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 { useSubscriptions } from '../../store/subscriptions.ts'
export default { export default {
name: 'Banner', name: 'Banner',

View File

@ -102,8 +102,8 @@ 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 Modal from '../Atoms/Modal.vue' import Modal from '../Atoms/Modal.vue'
@ -113,10 +113,10 @@ import PlaylistPlayIcon from 'vue-material-design-icons/PlaylistPlay.vue'
import PlaylistRemoveIcon from 'vue-material-design-icons/PlaylistRemove.vue' import PlaylistRemoveIcon from 'vue-material-design-icons/PlaylistRemove.vue'
import StopIcon from 'vue-material-design-icons/Stop.vue' import StopIcon from 'vue-material-design-icons/Stop.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { filenameFromUrl } from '../../utils/url.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 { usePlayer } from '../../store/player.ts'
export default { export default {
name: 'Episode', name: 'Episode',

View File

@ -12,16 +12,16 @@
</template> </template>
<script> <script>
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 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 { usePlayer } from '../../store/player.ts'
import { useSettings } from '../../store/settings.js' import { useSettings } from '../../store/settings.ts'
export default { export default {
name: 'Episodes', name: 'Episodes',

View File

@ -29,10 +29,10 @@ 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 { mapState } from 'pinia'
import { showError } from '../../utils/toast.js' import { showError } from '../../utils/toast.ts'
import { useSubscriptions } from '../../store/subscriptions.js' import { useSubscriptions } from '../../store/subscriptions.ts'
export default { export default {
name: 'Favorite', name: 'Favorite',

View File

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

View File

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

View File

@ -16,8 +16,8 @@
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',

View File

@ -10,7 +10,7 @@
<script> <script>
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',

View File

@ -7,9 +7,9 @@
</template> </template>
<script> <script>
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',

View File

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

View File

@ -44,7 +44,7 @@ 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 { useSettings } from '../../store/settings.ts'
export default { export default {
name: 'Filters', name: 'Filters',

View File

@ -23,7 +23,7 @@ import PlusIcon from 'vue-material-design-icons/Plus.vue'
import SpeedometerIcon from 'vue-material-design-icons/Speedometer.vue' import SpeedometerIcon from 'vue-material-design-icons/Speedometer.vue'
import SpeedometerMediumIcon from 'vue-material-design-icons/SpeedometerMedium.vue' import SpeedometerMediumIcon from 'vue-material-design-icons/SpeedometerMedium.vue'
import SpeedometerSlowIcon from 'vue-material-design-icons/SpeedometerSlow.vue' import SpeedometerSlowIcon from 'vue-material-design-icons/SpeedometerSlow.vue'
import { usePlayer } from '../../store/player.js' import { usePlayer } from '../../store/player.ts'
export default { export default {
name: 'Speed', name: 'Speed',

View File

@ -47,9 +47,9 @@ 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 { toFeedUrl } from '../../utils/url.ts'
import { useSubscriptions } from '../../store/subscriptions.js' import { useSubscriptions } from '../../store/subscriptions.ts'
export default { export default {
name: 'Subscription', name: 'Subscription',

View File

@ -43,8 +43,8 @@ import Loading from '../Atoms/Loading.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue' import PlusIcon from 'vue-material-design-icons/Plus.vue'
import Settings from '../Settings/Settings.vue' import Settings from '../Settings/Settings.vue'
import Subscription from './Subscription.vue' import Subscription from './Subscription.vue'
import { showError } from '../../utils/toast.js' import { showError } from '../../utils/toast.ts'
import { useSubscriptions } from '../../store/subscriptions.js' import { useSubscriptions } from '../../store/subscriptions.ts'
export default { export default {
name: 'Subscriptions', name: 'Subscriptions',

View File

@ -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'
const Vue = createApp(App) const Vue = createApp(App)
const pinia = createPinia() const pinia = createPinia()
Vue.mixin({ methods: { t, n } })
Vue.use(pinia) Vue.use(pinia)
Vue.use(router) Vue.use(router)
Vue.mount('#content') Vue.mount('#content')

View File

@ -1,18 +1,19 @@
import type { EpisodeInterface } from '../utils/types'
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'
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,11 +30,11 @@ 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()
@ -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,7 +82,13 @@ export const usePlayer = defineStore('player', {
this.episode = null this.episode = null
}, },
time() { time() {
this.episode.action = { if (!this.podcastUrl || !this.episode?.url) {
return
}
this.episode = {
...this.episode,
action: {
podcast: this.podcastUrl, podcast: this.podcastUrl,
episode: this.episode.url, episode: this.episode.url,
guid: this.episode.guid, guid: this.episode.guid,
@ -90,15 +97,17 @@ export const usePlayer = defineStore('player', {
started: Math.round(this.started), started: Math.round(this.started),
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
}, },
}, },

View File

@ -1,31 +0,0 @@
import { getCookie, setCookie } from '../utils/cookies.js'
import { defineStore } from 'pinia'
export const useSettings = defineStore('settings', {
state: () => {
try {
const filters = JSON.parse(getCookie('repod.filters'))
return {
filters: {
listened: filters.listened,
listening: filters.listening,
unlistened: filters.unlistened,
},
}
} catch {
return {
filters: {
listened: true,
listening: true,
unlistened: true,
},
}
}
},
actions: {
setFilters(filters) {
this.filters = { ...this.filters, ...filters }
setCookie('repod.filters', JSON.stringify(this.filters), 365)
},
},
})

36
src/store/settings.ts Normal file
View File

@ -0,0 +1,36 @@
import { getCookie, setCookie } from '../utils/cookies'
import type { FiltersInterface } from '../utils/types'
import { defineStore } from 'pinia'
export const useSettings = defineStore('settings', {
state: () => {
const cookie = getCookie('repod.filters')
if (cookie) {
try {
const filters = JSON.parse(cookie)
return {
filters: {
listened: filters.listened,
listening: filters.listening,
unlistened: filters.unlistened,
},
}
} catch {}
}
return {
filters: {
listened: true,
listening: true,
unlistened: true,
},
}
},
actions: {
setFilters(filters: FiltersInterface) {
this.filters = { ...this.filters, ...filters }
setCookie('repod.filters', JSON.stringify(this.filters), 365)
},
},
})

View File

@ -1,4 +1,4 @@
import { getCookie, setCookie } from '../utils/cookies.js' import { getCookie, setCookie } from '../utils/cookies'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'

View File

@ -4,7 +4,7 @@
* @param {string} name Nom du cookie à récupérer * @param {string} name Nom du cookie à récupérer
* @return {string|null} * @return {string|null}
*/ */
export const getCookie = (name) => { 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;`

View File

@ -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)
}
}

View File

@ -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]
) )

View File

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

View File

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

View File

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

33
src/utils/types.ts Normal file
View File

@ -0,0 +1,33 @@
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
duration?: string
action?: EpisodeActionInterface
}
export interface FiltersInterface {
listened: boolean
listening: boolean
unlistened: boolean
}

View File

@ -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
View 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()

View File

@ -25,7 +25,7 @@ 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 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'
export default { export default {

View File

@ -24,7 +24,7 @@ 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 { useSubscriptions } from '../store/subscriptions.ts'
export default { export default {
name: 'Home', name: 'Home',

19
tsconfig.json Normal file
View 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,
},
}

View File

@ -19,7 +19,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 },
) )