Commit 97f20e23 authored by Kourser's avatar Kourser
Browse files

feat: swipe an episode, with the action of your choosing

Answering "how do I add an episode to à écouter plus tard?": you held
your finger on it and picked "Ajouter aux favoris". A long press nobody
discovers, labelled with a word that did not name the list it filled.

So the vocabulary is now one word throughout — context menu, the podcast
page's filter, accessibility labels — and a swipe does the job. Which
action each direction triggers is a setting, since what one listener
wants under their thumb is not what the next one does; the default puts
"à écouter plus tard" on the right swipe, that list having had no visible
way in at all. Every action stays in the context menu, so a swipe is
only ever a shortcut.

Fixes a bug the new list made obvious: the star and the progress bar
rendered as if untouched in Nouveautés, the queue and the new list,
because only a podcast's own page called loadPlayStates. Swiping there
offered to add an episode that was already in the list.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent 22712996
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -7,6 +7,23 @@ adopte le [versionnage sémantique](https://semver.org/lang/fr/).

## [Non publié]

### Ajouté
- **Balayage d'un épisode** : un balayage vers la droite ou vers la gauche
  déclenche l'action de votre choix — à écouter plus tard, file d'attente,
  marquer comme lu, téléchargement, ou rien. Réglable dans Réglages › Balayage
  d'un épisode. Tout reste accessible en gardant le doigt sur un épisode.

### Modifié
- **« Favoris » devient « À écouter plus tard »** partout : menu contextuel,
  filtre d'une fiche de podcast, étiquettes d'accessibilité. Le mot « favori »
  ne disait pas à quoi servait la liste.

### Corrigé
- **État des épisodes dans les listes transversales** : dans « Nouveautés », la
  file d'attente et « À écouter plus tard », l'étoile et la barre de progression
  s'affichaient toujours à l'état neuf. Seule la fiche d'un podcast chargeait
  ces informations.

### Ajouté
- **À écouter plus tard** : les épisodes mis en favori, toutes émissions
  confondues, dans une liste à part sur l'accueil. Les favoris n'étaient
+5 −1
Original line number Diff line number Diff line
@@ -218,10 +218,13 @@ final class AppModel {

    func loadInbox() async {
        inbox = (try? await store.recentEpisodes(limit: 100)) ?? []
        await loadPlayStates(for: inbox)
    }

    func favoriteEpisodes() async -> [Episode] {
        (try? await store.favoriteEpisodes()) ?? []
        let episodes = (try? await store.favoriteEpisodes()) ?? []
        await loadPlayStates(for: episodes)
        return episodes
    }

    func recentlyPlayed() async -> [Episode] {
@@ -694,6 +697,7 @@ final class AppModel {

    func loadQueue() async {
        queue = (try? await store.queuedEpisodes()) ?? []
        await loadPlayStates(for: queue)
    }

    func isQueued(_ episode: Episode) -> Bool {
+71 −2
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ import PlaybackKit
/// download. Shared between the podcast detail screen, the inbox and the queue.
struct EpisodeListRow: View {
    @Environment(AppModel.self) private var model
    @Environment(SwipeSettings.self) private var swipeSettings
    @Environment(DownloadController.self) private var downloads
    @Environment(PlaybackController.self) private var playback
    let episode: Episode
@@ -32,7 +33,7 @@ struct EpisodeListRow: View {
                        Image(systemName: "star.fill")
                            .foregroundStyle(.yellow)
                            .font(.caption)
                            .accessibilityLabel(Text("Favori"))
                            .accessibilityLabel(Text("À écouter plus tard"))
                    }
                    Text(episode.title)
                        .font(.headline)
@@ -55,6 +56,12 @@ struct EpisodeListRow: View {
        }
        .padding(.vertical, 2)
        .contextMenu { contextActions }
        .swipeActions(edge: .leading, allowsFullSwipe: true) {
            swipeButton(for: swipeSettings.leading)
        }
        .swipeActions(edge: .trailing, allowsFullSwipe: false) {
            swipeButton(for: swipeSettings.trailing)
        }
        #if os(macOS)
        // A window-modal sheet is heavy-handed for notes on the Mac.
        .popover(isPresented: $showingNotes, arrowEdge: .trailing) {
@@ -153,6 +160,67 @@ struct EpisodeListRow: View {
        }
    }

    /// A swipe button for the action the user picked for that edge. Labels stay
    /// short here — a swipe reveals a narrow strip, not a menu — and the whole
    /// thing is skipped for radios, which have no episode state to change.
    @ViewBuilder private func swipeButton(for action: EpisodeSwipeAction) -> some View {
        if !isRadio {
            switch action {
            case .none:
                EmptyView()
            case .listenLater:
                Button {
                    Task { await model.toggleFavorite(episode) }
                } label: {
                    Label(isFavorite ? "Retirer" : "Plus tard",
                          systemImage: isFavorite ? "star.slash" : "star")
                }
                .tint(.orange)
            case .queue:
                if model.isQueued(episode) {
                    Button {
                        Task { await model.dequeue(episode) }
                    } label: {
                        Label("Retirer", systemImage: "text.badge.minus")
                    }
                    .tint(.gray)
                } else {
                    Button {
                        Task { await model.enqueue(episode) }
                    } label: {
                        Label("File", systemImage: "text.badge.plus")
                    }
                    .tint(.accentColor)
                    .disabled(episode.enclosureURL == nil)
                }
            case .played:
                Button {
                    Task { await model.markPlayed(episode, !isPlayed) }
                } label: {
                    Label(isPlayed ? "Non lu" : "Lu",
                          systemImage: isPlayed ? "circle" : "checkmark.circle")
                }
                .tint(.green)
            case .download:
                if downloads.status(for: episode) == .downloaded {
                    Button(role: .destructive) {
                        downloads.delete(episode)
                    } label: {
                        Label("Supprimer", systemImage: "trash")
                    }
                } else {
                    Button {
                        downloads.download(episode)
                    } label: {
                        Label("Télécharger", systemImage: "arrow.down.circle")
                    }
                    .tint(.blue)
                    .disabled(episode.enclosureURL == nil)
                }
            }
        }
    }

    @ViewBuilder private var contextActions: some View {
        if let link = episode.websiteURL ?? episode.enclosureURL {
            ShareLink(item: link) {
@@ -186,7 +254,8 @@ struct EpisodeListRow: View {
            Button {
                Task { await model.toggleFavorite(episode) }
            } label: {
                Label(isFavorite ? "Retirer des favoris" : "Ajouter aux favoris",
                Label(isFavorite ? "Retirer d'« À écouter plus tard »"
                                : "Ajouter à « À écouter plus tard »",
                      systemImage: isFavorite ? "star.slash" : "star")
            }

+1 −1
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@ struct LaterView: View {
                ContentUnavailableView(
                    "Rien en attente",
                    systemImage: "star",
                    description: Text("Les épisodes que vous mettez en favori se retrouvent ici, prêts à être écoutés plus tard.")
                    description: Text("Balayez un épisode vers la droite, ou gardez le doigt dessus, pour le mettre de côté ici.")
                )
                .frame(maxWidth: .infinity)
                .listRowBackground(Color.clear)
+1703 −643

File changed.

Preview size limit exceeded, changes collapsed.

Loading