Commit b1926178 authored by Kourser's avatar Kourser
Browse files

fix(ios): restore the search field during playback, and unstick Nouveautés

Two regressions reported by a user on iPhone, both reproduced on an
iOS 26 simulator.

iOS 26 floats the search field at the bottom of the screen, which is
where RootView reserves space for the now-playing bar via a bottom
safe-area inset. The two competed for the same strip and the field simply
stopped being rendered once an episode was loaded — not covered, absent.
Pinning it under the title keeps both.

Nouveautés was worse than it looked: the empty state told the user to
pull to refresh, but ContentUnavailableView on its own does not scroll,
so .refreshable never fired. An inbox that was empty for any reason could
never be filled from the screen that shows it. The empty state now lives
inside the List, and refreshAllFeeds reloads the inbox itself so a
refresh from anywhere — this screen, the sidebar, the background task —
keeps it current instead of waiting for the view to appear again.

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

## [Non publié]

### Corrigé
- **iOS — la recherche disparaissait pendant la lecture** : iOS 26 place le
  champ de recherche en bas de l'écran, exactement là où la barre de lecture
  réserve sa place. Le champ n'était plus affiché du tout dès qu'un épisode
  était chargé. Il est désormais épinglé sous le titre, où les deux ne se
  disputent plus la même zone.
- **« Nouveautés » restait vide sans moyen d'en sortir** : l'écran invitait à
  tirer pour actualiser, mais le geste n'avait aucun effet — l'état vide n'était
  pas défilable. Il l'est maintenant, et la liste se recharge après chaque
  rafraîchissement des flux, d'où qu'il vienne, au lieu d'attendre une nouvelle
  ouverture de l'écran.

## [1.0.0] - 2026-09-01

### Ajouté
+2 −0
Original line number Diff line number Diff line
@@ -406,6 +406,7 @@ final class AppModel {
    func refreshAllFeeds() async {
        for podcast in podcasts { _ = await refreshFeed(podcast) }
        await load()
        await loadInbox()
    }

    /// Re-fetches a single subscription's feed.
@@ -421,6 +422,7 @@ final class AppModel {
        var newCount = 0
        for podcast in podcasts { newCount += await refreshFeed(podcast) }
        await load()
        await loadInbox()
        await notifications.postNewEpisodes(count: newCount)
    }

+12 −7
Original line number Diff line number Diff line
@@ -5,26 +5,31 @@ struct InboxView: View {
    @Environment(AppModel.self) private var model

    var body: some View {
        Group {
        // The empty state lives INSIDE the List rather than replacing it: a
        // `ContentUnavailableView` on its own does not scroll, so `.refreshable`
        // never fired and the "tirez pour actualiser" hint was a dead end —
        // exactly when the user most needs to refresh.
        List {
            if model.inbox.isEmpty {
                ContentUnavailableView(
                    "Aucune nouveauté",
                    systemImage: "tray",
                    description: Text("Les épisodes récents de vos abonnements apparaîtront ici. Tirez pour actualiser.")
                )
                .frame(maxWidth: .infinity)
                .listRowBackground(Color.clear)
                .listRowSeparator(.hidden)
            } else {
                List {
                ForEach(model.inbox) { episode in
                    EpisodeListRow(episode: episode, showArtwork: true)
                }
            }
        }
        }
        .navigationTitle("Nouveautés")
        // `refreshAllFeeds` reloads the inbox itself, so a refresh triggered
        // anywhere — here, the sidebar, or the background task — keeps this
        // list current instead of waiting for the screen to appear again.
        .refreshable { await model.refreshAllFeeds() }
        .task { await model.loadInbox() }
        .refreshable {
            await model.refreshAllFeeds()
            await model.loadInbox()
        }
    }
}
+12 −0
Original line number Diff line number Diff line
@@ -82,7 +82,19 @@ struct RootView: View {
                }
            }
            .navigationTitle("Skingomz")
            // iOS 26 floats the search field at the BOTTOM of the screen, where
            // the now-playing bar's safe-area inset displaces it entirely — the
            // field simply stops being rendered once something is playing. Pin
            // it under the title instead, so the two never compete.
            #if os(iOS)
            .searchable(
                text: $searchText,
                placement: .navigationBarDrawer(displayMode: .always),
                prompt: "Rechercher (podcasts, épisodes)"
            )
            #else
            .searchable(text: $searchText, prompt: "Rechercher (podcasts, épisodes)")
            #endif
            .refreshable { await model.refreshAllFeeds() }
            .toolbar { toolbarContent }
        } detail: {