Commit fc77469b authored by Kourser's avatar Kourser
Browse files

feat: sync and refresh feeds on their own

syncNow() had exactly one caller — the button in the sync screen — so a
user with a gpodder server only ever synced by hand, as one reported.
Feeds were no better: nothing refreshed them except a pull-to-refresh and
a background task the system runs when it feels like it, with a four-hour
floor.

Both now run at launch and when the app comes back to the foreground,
throttled to once every thirty minutes so switching apps does not hammer
anyone's feeds, and the background pass syncs before it refreshes. Sync
goes first either way: a subscription added on another device is then
picked up in the same pass instead of a day later.

The automatic path takes a separate entry point. syncNow() reports that
sync is not configured, which is right for someone who just tapped the
button and wrong for something running on its own, so syncIfConfigured()
stays silent instead.

Verified on an iOS 26 simulator: one pass at launch, and an immediate
return to the foreground is skipped by the throttle.

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

## [Non publié]

### Modifié
- **Synchronisation et flux automatiques** : la synchronisation ne partait qu'en
  appuyant sur « Synchroniser », et les flux n'étaient rafraîchis que par un
  tirer-pour-actualiser. Les deux se font désormais seuls au lancement et au
  retour dans l'app — au plus une fois toutes les 30 minutes — ainsi que dans le
  rafraîchissement en arrière-plan, qui ne synchronisait rien jusqu'ici. La
  synchronisation passe en premier, pour qu'un abonnement ajouté sur un autre
  appareil soit pris dans la même passe.

### 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
+32 −0
Original line number Diff line number Diff line
@@ -403,6 +403,35 @@ final class AppModel {
    }

    /// Re-fetches every subscription's feed to pull in new episodes.
    /// Automatic entry point for sync. `syncNow()` tells the user when sync is
    /// not configured — right for someone who just tapped the button, wrong for
    /// something running on its own — so this variant stays silent instead.
    func syncIfConfigured() async {
        guard syncSettings.credentials() != nil else { return }
        await syncNow()
    }

    /// Syncs and refreshes every feed, at launch and whenever the app comes
    /// back to the foreground — but at most once per `catchUpInterval`, so
    /// switching apps back and forth does not hammer anyone's feeds.
    ///
    /// Sync goes first: it can add or drop subscriptions that the refresh then
    /// walks. The timestamp is written up front rather than on success, so two
    /// overlapping triggers cannot both start a pass.
    func catchUpIfStale(now: Date = .now) async {
        let defaults = UserDefaults.standard
        if let last = defaults.object(forKey: Self.lastCatchUpKey) as? Date,
           now.timeIntervalSince(last) < Self.catchUpInterval {
            return
        }
        defaults.set(now, forKey: Self.lastCatchUpKey)
        await syncIfConfigured()
        await refreshAllFeeds()
    }

    private static let lastCatchUpKey = "library.lastCatchUpDate"
    static let catchUpInterval: TimeInterval = 30 * 60

    func refreshAllFeeds() async {
        for podcast in podcasts { _ = await refreshFeed(podcast) }
        await load()
@@ -419,6 +448,9 @@ final class AppModel {
    /// feed — triggering auto-download — and notifies about new episodes.
    func backgroundRefresh() async {
        await load()
        // Sync first, so a subscription added on another device is picked up in
        // the same pass rather than a day later.
        await syncIfConfigured()
        var newCount = 0
        for podcast in podcasts { newCount += await refreshFeed(podcast) }
        await load()
+3 −0
Original line number Diff line number Diff line
@@ -205,6 +205,9 @@ struct RootView: View {
            #endif
        }
        .onChange(of: scenePhase) { _, phase in
            if phase == .active {
                Task { await model.catchUpIfStale() }
            }
            if phase == .background {
                Task { await model.persistPlaybackPosition() }
            }
+3 −0
Original line number Diff line number Diff line
@@ -62,6 +62,9 @@ struct SkingomzApp: App {
                    reviewPrompter.noteLaunch()
                    await model.start()
                    await updateChecker.check()
                    // Feeds were only ever refreshed by a pull-to-refresh or an
                    // opportunistic background task the system may rarely run.
                    await model.catchUpIfStale()
                }
        }
        #if os(macOS)