Commit 1d03cde2 authored by Kourser's avatar Kourser
Browse files

Add app reset (Réglages) and pull-to-refresh feeds



- Réglages sidebar screen with a destructive "Réinitialiser l'application"
  button (confirmation alert). Reset wipes the database, downloaded files, play
  state, queue, and sync settings (UserDefaults + Keychain), and stops playback.
- Pull-to-refresh: swipe down on the library re-fetches all subscriptions'
  feeds; on a podcast's screen it refreshes that feed (new episodes via the
  existing upsert/dedup).
- Supporting API: LibraryStore.deleteAllData, PlaybackController.stop,
  DownloadController.reset, SyncSettings.reset, KeychainStore.delete,
  AppModel.refreshAllFeeds/refresh/resetApplication. + reset test.

46 package tests green; iOS + macOS build; Réglages screen verified in simulator.

Co-Authored-By: default avatarClaude <claude@anthropic.com>
parent abeb3bdf
Loading
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -60,6 +60,16 @@ public final class PlaybackController {
        notify()
    }

    /// Stops playback and clears the current item (used by the in-app reset).
    public func stop() {
        engine.pause()
        isPlaying = false
        currentEpisode = nil
        currentTime = 0
        duration = 0
        notify()
    }

    public func resume() {
        guard currentEpisode != nil else { return }
        engine.setRate(playbackRate)
+12 −0
Original line number Diff line number Diff line
@@ -164,4 +164,16 @@ public final class LibraryStore: Sendable {
            try PodcastRecord.deleteOne(db, key: podcastId)
        }
    }

    /// Wipes all stored data (subscriptions, episodes, queue, downloads,
    /// play state). Used by the in-app reset.
    public func deleteAllData() async throws {
        _ = try await dbQueue.write { db in
            try PlayStateRecord.deleteAll(db)
            try QueueRecord.deleteAll(db)
            try DownloadRecord.deleteAll(db)
            try EpisodeRecord.deleteAll(db)
            try PodcastRecord.deleteAll(db)
        }
    }
}
+27 −0
Original line number Diff line number Diff line
import Foundation
import Testing
import StorageKit
import PodcastModel

@Suite struct ResetTests {
    @Test func deleteAllDataClearsEverything() async throws {
        let store = try LibraryStore.inMemory()
        let podcast = Podcast(feedURL: URL(string: "https://example.com/f.xml")!, title: "Cast")
        let stored = try await store.save(
            podcast: podcast,
            episodes: [Episode(guid: "g0", title: "E0", enclosureURL: URL(string: "https://example.com/0.mp3"))]
        )
        let saved = try await store.episodes(forPodcastID: stored.id)
        try await store.enqueue(episodeID: saved[0].id)
        try await store.markDownloaded(episodeID: saved[0].id, fileName: "a.mp3")
        try await store.setPlayState(episodeID: saved[0].id, position: 5, isPlayed: false)

        try await store.deleteAllData()

        #expect(try await store.allPodcasts().isEmpty)
        #expect(try await store.queuedEpisodes().isEmpty)
        #expect(try await store.allDownloads().isEmpty)
        #expect(try await store.episodes(forPodcastID: stored.id).isEmpty)
        #expect(try await store.playState(forEpisodeID: saved[0].id) == nil)
    }
}
+29 −0
Original line number Diff line number Diff line
@@ -215,6 +215,35 @@ final class AppModel {
        await loadQueue()
    }

    /// Re-fetches every subscription's feed to pull in new episodes.
    func refreshAllFeeds() async {
        for podcast in podcasts {
            if let feed = try? await fetcher.fetch(url: podcast.feedURL) {
                try? await store.save(podcast: feed.podcast, episodes: feed.episodes)
            }
        }
        await load()
    }

    /// Re-fetches a single subscription's feed.
    func refresh(_ podcast: Podcast) async {
        if let feed = try? await fetcher.fetch(url: podcast.feedURL) {
            try? await store.save(podcast: feed.podcast, episodes: feed.episodes)
        }
    }

    /// Erases all local data and configuration (subscriptions, episodes, queue,
    /// downloads, play state, sync settings) and stops playback.
    func resetApplication() async {
        playback?.stop()
        try? await store.deleteAllData()
        downloads.reset()
        syncSettings.reset()
        playStates = [:]
        await load()
        await loadQueue()
    }

    // MARK: Playback

    /// Plays an episode, resuming from its saved position and preferring a
+7 −0
Original line number Diff line number Diff line
@@ -73,6 +73,13 @@ final class DownloadController {
        Task { try? await store.removeDownloadRecord(episodeID: episode.id) }
    }

    /// Removes every downloaded file and clears status (used by the reset).
    func reset() {
        statuses = [:]
        try? FileManager.default.removeItem(at: directory)
        try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
    }

    /// Deterministic file name so it can be recomputed without a DB lookup.
    private func fileName(for episode: Episode) -> String {
        let ext = episode.enclosureURL?.pathExtension ?? ""
Loading