Merge pull request 'select all episodes (fix #81)' (#220) from multiselect into main
All checks were successful
repod / xml (push) Successful in 23s
repod / php (push) Successful in 54s
repod / nodejs (push) Successful in 1m2s
repod / release (push) Has been skipped

Reviewed-on: #220
This commit is contained in:
Michel Roux 2024-12-11 14:01:16 +00:00
commit 7cd3694e85
5 changed files with 236 additions and 76 deletions

View File

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

View File

@ -2,26 +2,111 @@
<div>
<Loading 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
v-for="episode in filteredEpisodes"
:key="episode.guid"
:display-actions="episodes.every((e) => !e.selected)"
:episode="episode"
:url="url" />
:url="url"
@select="episode.selected = !episode.selected" />
</ul>
</div>
</template>
<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 type { EpisodeInterface } from '../../utils/types.ts'
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 { decodeUrl } from '../../utils/url.ts'
import { generateUrl } from '@nextcloud/router'
import { mapState } from 'pinia'
import { showError } from '../../utils/toast.ts'
import { t } from '@nextcloud/l10n'
import { usePlayer } from '../../store/player.ts'
import { useSettings } from '../../store/settings.ts'
@ -30,6 +115,13 @@ export default {
components: {
Episode,
Loading,
NcActionButton,
NcAvatar,
NcListItem,
PlaylistPlayIcon,
PlaylistRemoveIcon,
SelectAllIcon,
SelectRemoveIcon,
},
data: () => ({
episodes: [] as EpisodeInterface[],
@ -59,8 +151,8 @@ export default {
watch: {
episode() {
if (this.episode) {
this.episodes = this.episodes.map((e) =>
e.url === this.episode?.url ? this.episode : e,
this.episodes = this.episodes.map((episode) =>
episode.url === this.episode?.url ? this.episode : episode,
)
}
},
@ -88,6 +180,36 @@ export default {
methods: {
hasEnded,
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>
<style scoped>
.progress {
margin-top: 0.4rem;
}
</style>

View File

@ -1,3 +1,4 @@
import { durationToSeconds, formatEpisodeTimestamp } from './time'
import type { EpisodeInterface } from './types'
export const hasEnded = (episode: EpisodeInterface) =>
@ -14,3 +15,17 @@ export const isListening = (episode: EpisodeInterface) =>
episode.action.action.toLowerCase() === 'play' &&
episode.action.position > 0 &&
!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
action?: EpisodeActionInterface
selected?: boolean
}
export interface FiltersInterface {

View File

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