select all episodes (fix #81) #220

Merged
Xefir merged 4 commits from multiselect into main 2024-12-11 14:01:21 +00:00
5 changed files with 236 additions and 76 deletions

View File

@ -33,7 +33,7 @@
</NcActionButton> </NcActionButton>
</template> </template>
<template #extra> <template #extra>
<NcActions> <NcActions v-if="displayActions">
<NcActionButton <NcActionButton
v-if="episode.duration" v-if="episode.duration"
:aria-label="t('repod', 'Read')" :aria-label="t('repod', 'Read')"
@ -41,7 +41,7 @@
:model-value="hasEnded(episode)" :model-value="hasEnded(episode)"
:name="t('repod', 'Read')" :name="t('repod', 'Read')"
:title="t('repod', 'Read')" :title="t('repod', 'Read')"
@click="markAs(episode, !hasEnded(episode))"> @click="read(episode, !hasEnded(episode))">
<template #icon> <template #icon>
<PlaylistPlayIcon v-if="!hasEnded(episode)" :size="20" /> <PlaylistPlayIcon v-if="!hasEnded(episode)" :size="20" />
<PlaylistRemoveIcon v-if="hasEnded(episode)" :size="20" /> <PlaylistRemoveIcon v-if="hasEnded(episode)" :size="20" />
@ -77,7 +77,12 @@
<NcAvatar <NcAvatar
:display-name="episode.name" :display-name="episode.name"
:is-no-user="true" :is-no-user="true"
:url="episode.image" /> :url="episode.image"
@click.stop="$emit('select', episode)">
<template #icon>
<CheckIcon v-if="episode.selected" class="progress" :size="20" />
</template>
</NcAvatar>
</template> </template>
<template #indicator> <template #indicator>
<NcProgressBar <NcProgressBar
@ -101,13 +106,9 @@ import {
NcModal, NcModal,
NcProgressBar, NcProgressBar,
} from '@nextcloud/vue' } from '@nextcloud/vue'
import { import { hasEnded, isListening, markAs } from '../../utils/status.ts'
durationToSeconds,
formatEpisodeTimestamp,
formatLocaleDate,
} from '../../utils/time.ts'
import { hasEnded, isListening } from '../../utils/status.ts'
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import CheckIcon from 'vue-material-design-icons/Check.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 type { EpisodeInterface } from '../../utils/types.ts'
import Modal from '../Atoms/Modal.vue' import Modal from '../Atoms/Modal.vue'
@ -118,6 +119,7 @@ import PlaylistRemoveIcon from 'vue-material-design-icons/PlaylistRemove.vue'
import StopIcon from 'vue-material-design-icons/Stop.vue' import StopIcon from 'vue-material-design-icons/Stop.vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { filenameFromUrl } from '../../utils/url.ts' import { filenameFromUrl } from '../../utils/url.ts'
import { formatLocaleDate } from '../../utils/time.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 { t } from '@nextcloud/l10n'
@ -126,6 +128,7 @@ import { usePlayer } from '../../store/player.ts'
export default { export default {
name: 'Episode', name: 'Episode',
components: { components: {
CheckIcon,
DownloadIcon, DownloadIcon,
Modal, Modal,
NcActionButton, NcActionButton,
@ -142,6 +145,10 @@ export default {
StopIcon, StopIcon,
}, },
props: { props: {
displayActions: {
type: Boolean,
default: true,
},
episode: { episode: {
type: Object as () => EpisodeInterface, type: Object as () => EpisodeInterface,
required: true, required: true,
@ -155,6 +162,7 @@ export default {
required: true, required: true,
}, },
}, },
emits: ['select'],
data: () => ({ data: () => ({
loading: false, loading: false,
modalEpisode: null as EpisodeInterface | null, modalEpisode: null as EpisodeInterface | null,
@ -172,19 +180,10 @@ export default {
isCurrentEpisode(episode: EpisodeInterface) { isCurrentEpisode(episode: EpisodeInterface) {
return this.playerEpisode?.url === episode.url return this.playerEpisode?.url === episode.url
}, },
async markAs(episode: EpisodeInterface, read: boolean) { async read(episode: EpisodeInterface, read: boolean) {
try { try {
this.loading = true this.loading = true
episode.action = { episode = markAs(episode, read, this.url)
podcast: this.url,
episode: episode.url,
guid: episode.guid,
action: 'play',
timestamp: formatEpisodeTimestamp(new Date()),
started: episode.action?.started || 0,
position: read ? durationToSeconds(episode.duration || '') : 0,
total: durationToSeconds(episode.duration || ''),
}
await axios.post( await axios.post(
generateUrl('/apps/gpoddersync/episode_action/create'), generateUrl('/apps/gpoddersync/episode_action/create'),
[episode.action], [episode.action],

View File

@ -2,26 +2,111 @@
<div> <div>
<Loading v-if="loading" /> <Loading v-if="loading" />
<ul v-if="!loading"> <ul v-if="!loading">
<NcListItem
v-if="!episodes.every((e) => !e.selected)"
:active="true"
:force-display-actions="true"
:name="
n(
'repod',
'%n episode selected',
'%n episodes selected',
episodes.filter((e) => e.selected).length,
)
"
:one-line="true">
<template #actions>
<NcActionButton
v-if="
episodes
.filter((e) => e.selected)
.filter((e) => !hasEnded(e)).length
"
:aria-label="t('repod', 'Read all')"
:disabled="
!episodes
.filter((e) => e.selected)
.every((e) => e.duration)
"
:name="t('repod', 'Read all')"
:title="t('repod', 'Read all')"
@click="read(true)">
<template #icon>
<PlaylistPlayIcon :size="20" />
</template>
</NcActionButton>
<NcActionButton
v-if="
episodes
.filter((e) => e.selected)
.every((e) => hasEnded(e))
"
:aria-label="t('repod', 'Unread all')"
:disabled="
!episodes
.filter((e) => e.selected)
.every((e) => e.duration)
"
:name="t('repod', 'Unread all')"
:title="t('repod', 'Unread all')"
@click="read(false)">
<template #icon>
<PlaylistRemoveIcon :size="20" />
</template>
</NcActionButton>
</template>
<template #icon>
<NcAvatar
:display-name="t('repod', 'Select all')"
:is-no-user="true">
<template #icon>
<SelectAllIcon
v-if="
episodes.filter((e) => e.selected).length <
episodes.length
"
class="progress"
:size="20"
@click="select(true)" />
<SelectRemoveIcon
v-if="
episodes.filter((e) => e.selected).length >=
episodes.length
"
class="progress"
:size="20"
@click="select(false)" />
</template>
</NcAvatar>
</template>
</NcListItem>
<Episode <Episode
v-for="episode in filteredEpisodes" v-for="episode in filteredEpisodes"
:key="episode.guid" :key="episode.guid"
:display-actions="episodes.every((e) => !e.selected)"
:episode="episode" :episode="episode"
:url="url" /> :url="url"
@select="episode.selected = !episode.selected" />
</ul> </ul>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { hasEnded, isListening } from '../../utils/status.ts' import { NcActionButton, NcAvatar, NcListItem } from '@nextcloud/vue'
import { hasEnded, isListening, markAs } from '../../utils/status.ts'
import { n, t } from '@nextcloud/l10n'
import Episode from './Episode.vue' import Episode from './Episode.vue'
import type { EpisodeInterface } from '../../utils/types.ts' import type { EpisodeInterface } from '../../utils/types.ts'
import Loading from '../Atoms/Loading.vue' import Loading from '../Atoms/Loading.vue'
import PlaylistPlayIcon from 'vue-material-design-icons/PlaylistPlay.vue'
import PlaylistRemoveIcon from 'vue-material-design-icons/PlaylistRemove.vue'
import SelectAllIcon from 'vue-material-design-icons/SelectAll.vue'
import SelectRemoveIcon from 'vue-material-design-icons/SelectRemove.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,6 +115,13 @@ export default {
components: { components: {
Episode, Episode,
Loading, Loading,
NcActionButton,
NcAvatar,
NcListItem,
PlaylistPlayIcon,
PlaylistRemoveIcon,
SelectAllIcon,
SelectRemoveIcon,
}, },
data: () => ({ data: () => ({
episodes: [] as EpisodeInterface[], episodes: [] as EpisodeInterface[],
@ -59,8 +151,8 @@ export default {
watch: { watch: {
episode() { episode() {
if (this.episode) { if (this.episode) {
this.episodes = this.episodes.map((e) => this.episodes = this.episodes.map((episode) =>
e.url === this.episode?.url ? this.episode : e, episode.url === this.episode?.url ? this.episode : episode,
) )
} }
}, },
@ -88,6 +180,36 @@ export default {
methods: { methods: {
hasEnded, hasEnded,
isListening, isListening,
n,
t,
async read(read: boolean) {
try {
this.episodes = this.episodes.map((episode) =>
episode.selected ? markAs(episode, read, this.url) : episode,
)
await axios.post(
generateUrl('/apps/gpoddersync/episode_action/create'),
this.episodes
.filter((episode) => episode.selected)
.map((episode) => episode.action),
)
} catch (e) {
console.error(e)
showError(t('repod', 'Could not change the status of the episode'))
}
},
select(all: boolean) {
this.episodes = this.episodes.map((episode) => {
episode.selected = all
return episode
})
},
}, },
} }
</script> </script>
<style scoped>
.progress {
margin-top: 0.4rem;
}
</style>

View File

@ -1,3 +1,4 @@
import { durationToSeconds, formatEpisodeTimestamp } from './time'
import type { EpisodeInterface } from './types' import type { EpisodeInterface } from './types'
export const hasEnded = (episode: EpisodeInterface) => export const hasEnded = (episode: EpisodeInterface) =>
@ -14,3 +15,17 @@ export const isListening = (episode: EpisodeInterface) =>
episode.action.action.toLowerCase() === 'play' && episode.action.action.toLowerCase() === 'play' &&
episode.action.position > 0 && episode.action.position > 0 &&
!hasEnded(episode) !hasEnded(episode)
export const markAs = (episode: EpisodeInterface, read: boolean, url: string) => {
episode.action = {
podcast: url,
episode: episode.url,
guid: episode.guid,
action: 'play',
timestamp: formatEpisodeTimestamp(new Date()),
started: episode.action?.started || 0,
position: read ? durationToSeconds(episode.duration || '') : 0,
total: durationToSeconds(episode.duration || ''),
}
return episode
}

View File

@ -28,6 +28,7 @@ export interface EpisodeInterface {
} }
duration?: string duration?: string
action?: EpisodeActionInterface action?: EpisodeActionInterface
selected?: boolean
} }
export interface FiltersInterface { export interface FiltersInterface {

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Nextcloud 3.14159\n" "Project-Id-Version: Nextcloud 3.14159\n"
"Report-Msgid-Bugs-To: translations\\@example.com\n" "Report-Msgid-Bugs-To: translations\\@example.com\n"
"POT-Creation-Date: 2024-11-12 20:57+0000\n" "POT-Creation-Date: 2024-12-11 10:38+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -106,7 +106,7 @@ msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:16 #: /app/specialVueFakeDummyForL10nScript.js:16
#: /app/specialVueFakeDummyForL10nScript.js:17 #: /app/specialVueFakeDummyForL10nScript.js:17
#: /app/specialVueFakeDummyForL10nScript.js:32 #: /app/specialVueFakeDummyForL10nScript.js:41
msgid "Play" msgid "Play"
msgstr "" msgstr ""
@ -127,143 +127,166 @@ msgid "Open website"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:27 #: /app/specialVueFakeDummyForL10nScript.js:27
#: /app/specialVueFakeDummyForL10nScript.js:36
msgid "Could not change the status of the episode" msgid "Could not change the status of the episode"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:28 #: /app/specialVueFakeDummyForL10nScript.js:28
#: /app/specialVueFakeDummyForL10nScript.js:29 #: /app/specialVueFakeDummyForL10nScript.js:29
msgid "Could not fetch episodes"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:30 #: /app/specialVueFakeDummyForL10nScript.js:30
msgid "Rewind 10 seconds" msgid "Read all"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:31 #: /app/specialVueFakeDummyForL10nScript.js:31
msgid "Pause" #: /app/specialVueFakeDummyForL10nScript.js:32
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:33 #: /app/specialVueFakeDummyForL10nScript.js:33
msgid "Fast forward 30 seconds" msgid "Unread all"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:34 #: /app/specialVueFakeDummyForL10nScript.js:34
msgid "Select all"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:35 #: /app/specialVueFakeDummyForL10nScript.js:35
#: /app/specialVueFakeDummyForL10nScript.js:36 #: /app/specialVueFakeDummyForL10nScript.js:38
msgid "Mute" msgid "Could not fetch episodes"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:37 #: /app/specialVueFakeDummyForL10nScript.js:37
msgid "Unmute" msgid "%n episode selected"
msgstr "" msgid_plural "%n episodes selected"
msgstr[0] ""
#: /app/specialVueFakeDummyForL10nScript.js:38 msgstr[1] ""
msgid "Export subscriptions"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:39 #: /app/specialVueFakeDummyForL10nScript.js:39
msgid "Filtering episodes" msgid "Rewind 10 seconds"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:40 #: /app/specialVueFakeDummyForL10nScript.js:40
msgid "Show all" msgid "Pause"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:41
msgid "Listened"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:42 #: /app/specialVueFakeDummyForL10nScript.js:42
msgid "Listening" msgid "Fast forward 30 seconds"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:43 #: /app/specialVueFakeDummyForL10nScript.js:43
msgid "Unlistened"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:44 #: /app/specialVueFakeDummyForL10nScript.js:44
msgid "Import subscriptions"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:45 #: /app/specialVueFakeDummyForL10nScript.js:45
msgid "Import OPML file" msgid "Mute"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:46 #: /app/specialVueFakeDummyForL10nScript.js:46
msgid "Rate RePod ❤️" msgid "Unmute"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:47 #: /app/specialVueFakeDummyForL10nScript.js:47
msgid "Sleep timer" msgid "Export subscriptions"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:48 #: /app/specialVueFakeDummyForL10nScript.js:48
msgid "Minutes" msgid "Filtering episodes"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:49 #: /app/specialVueFakeDummyForL10nScript.js:49
msgid "Show all"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:50
msgid "Listened"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:51
msgid "Listening"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:52
msgid "Unlistened"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:53
msgid "Import subscriptions"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:54
msgid "Import OPML file"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:55
msgid "Rate RePod ❤️"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:56
msgid "Sleep timer"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:57
msgid "Minutes"
msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:58
msgid "%n min" msgid "%n min"
msgid_plural "%n mins" msgid_plural "%n mins"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: /app/specialVueFakeDummyForL10nScript.js:50 #: /app/specialVueFakeDummyForL10nScript.js:59
msgid "%n sec" msgid "%n sec"
msgid_plural "%n secs" msgid_plural "%n secs"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: /app/specialVueFakeDummyForL10nScript.js:51 #: /app/specialVueFakeDummyForL10nScript.js:60
msgid "Playback speed" msgid "Playback speed"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:52 #: /app/specialVueFakeDummyForL10nScript.js:61
#: /app/specialVueFakeDummyForL10nScript.js:53 #: /app/specialVueFakeDummyForL10nScript.js:62
#: /app/specialVueFakeDummyForL10nScript.js:54 #: /app/specialVueFakeDummyForL10nScript.js:63
msgid "Favorite" msgid "Favorite"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:55 #: /app/specialVueFakeDummyForL10nScript.js:64
msgid "Are you sure you want to delete this subscription?" msgid "Are you sure you want to delete this subscription?"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:56 #: /app/specialVueFakeDummyForL10nScript.js:65
msgid "Error while removing the feed" msgid "Error while removing the feed"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:57 #: /app/specialVueFakeDummyForL10nScript.js:66
msgid "You can only have 10 favorites" msgid "You can only have 10 favorites"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:58 #: /app/specialVueFakeDummyForL10nScript.js:67
msgid "Add a podcast" msgid "Add a podcast"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:59 #: /app/specialVueFakeDummyForL10nScript.js:68
msgid "Could not fetch subscriptions" msgid "Could not fetch subscriptions"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:60 #: /app/specialVueFakeDummyForL10nScript.js:69
msgid "Find a podcast" msgid "Find a podcast"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:61 #: /app/specialVueFakeDummyForL10nScript.js:70
msgid "Error loading feed" msgid "Error loading feed"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:62 #: /app/specialVueFakeDummyForL10nScript.js:71
msgid "Missing required app" msgid "Missing required app"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:63 #: /app/specialVueFakeDummyForL10nScript.js:72
msgid "Install GPodder Sync" msgid "Install GPodder Sync"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:64 #: /app/specialVueFakeDummyForL10nScript.js:73
msgid "Pin some subscriptions to see their latest updates" msgid "Pin some subscriptions to see their latest updates"
msgstr "" msgstr ""
#: /app/specialVueFakeDummyForL10nScript.js:65 #: /app/specialVueFakeDummyForL10nScript.js:74
msgid "No favorites" msgid "No favorites"
msgstr "" msgstr ""