Commit 973d3e83 authored by Kourser's avatar Kourser
Browse files

Inbox, library/episode search, subscription sort+tags, episode notes/share, version & commit



- "Nouveautés" sidebar entry (recent episodes across subscriptions); episode
  sort (newest/oldest) in podcast detail.
- Library search (.searchable): filters subscriptions by title and searches
  episodes; results shown in the sidebar.
- Subscription sort (recent/title) and tag filter in the sidebar; per-podcast
  tag editor (TagEditorView), backed by the podcast_tag table.
- Episode notes screen (EpisodeDetailView) with clickable links; "Détails" and
  "Partager" (ShareLink) actions on episode rows.
- Réglages "À propos": app version and git commit (stamped into Info.plist by a
  build phase; ENABLE_USER_SCRIPT_SANDBOXING disabled for the target so git runs).

59 package tests green; iOS + macOS build; inbox, settings (export/version),
player verified in the simulator.

Co-Authored-By: default avatarClaude <claude@anthropic.com>
parent 78131f4a
Loading
Loading
Loading
Loading
Loading
+21 −0
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@
				ABCDEF0123456789ABCD0007 /* Sources */,
				ABCDEF0123456789ABCD0008 /* Frameworks */,
				ABCDEF0123456789ABCD0009 /* Resources */,
				ABCDEF0123456789ABCD001D /* Stamp git commit */,
			);
			buildRules = (
			);
@@ -138,6 +139,24 @@
		};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
		ABCDEF0123456789ABCD001D /* Stamp git commit */ = {
			isa = PBXShellScriptBuildPhase;
			alwaysOutOfDate = 1;
			buildActionMask = 2147483647;
			files = (
			);
			inputPaths = (
			);
			name = "Stamp git commit";
			outputPaths = (
			);
			runOnlyForDeploymentPostprocessing = 0;
			shellPath = /bin/sh;
			shellScript = "COMMIT=$(git -C \"$SRCROOT\" rev-parse --short HEAD 2>/dev/null || echo unknown)\nPLIST=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\nif [ -f \"$PLIST\" ]; then\n  /usr/libexec/PlistBuddy -c \"Set :GitCommit $COMMIT\" \"$PLIST\" 2>/dev/null || /usr/libexec/PlistBuddy -c \"Add :GitCommit string $COMMIT\" \"$PLIST\"\nfi\n";
		};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
		ABCDEF0123456789ABCD0007 /* Sources */ = {
			isa = PBXSourcesBuildPhase;
@@ -245,6 +264,7 @@
				CURRENT_PROJECT_VERSION = 1;
				DEVELOPMENT_TEAM = 88NB46UUP7;
				ENABLE_PREVIEWS = YES;
				ENABLE_USER_SCRIPT_SANDBOXING = NO;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_FILE = Info.plist;
				INFOPLIST_KEY_CFBundleDisplayName = Skingomz;
@@ -278,6 +298,7 @@
				CURRENT_PROJECT_VERSION = 1;
				DEVELOPMENT_TEAM = 88NB46UUP7;
				ENABLE_PREVIEWS = YES;
				ENABLE_USER_SCRIPT_SANDBOXING = NO;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_FILE = Info.plist;
				INFOPLIST_KEY_CFBundleDisplayName = Skingomz;
+28 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@ final class AppModel {
    private(set) var queue: [Episode] = []
    private(set) var playStates: [UUID: PlayState] = [:]
    private(set) var favorites: Set<UUID> = []
    private(set) var inbox: [Episode] = []
    private(set) var podcastTags: [UUID: [String]] = [:]
    var errorMessage: String?
    var isAdding = false
    var isSyncing = false
@@ -166,6 +168,30 @@ final class AppModel {
        } catch {
            errorMessage = error.localizedDescription
        }
        podcastTags = (try? await store.allPodcastTags()) ?? [:]
    }

    // MARK: Browse / search / tags

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

    func searchEpisodes(_ query: String) async -> [Episode] {
        (try? await store.searchEpisodes(query)) ?? []
    }

    func tags(for podcast: Podcast) -> [String] {
        podcastTags[podcast.id] ?? []
    }

    func allTags() -> [String] {
        Set(podcastTags.values.flatMap { $0 }).sorted()
    }

    func setTags(_ tags: [String], for podcast: Podcast) async {
        try? await store.setTags(tags, forPodcastID: podcast.id)
        podcastTags = (try? await store.allPodcastTags()) ?? [:]
    }

    func subscribe(urlString: String) async -> Bool {
@@ -242,6 +268,8 @@ final class AppModel {
        syncSettings.reset()
        playStates = [:]
        favorites = []
        inbox = []
        podcastTags = [:]
        await load()
        await loadQueue()
    }
+56 −0
Original line number Diff line number Diff line
import SwiftUI
import PodcastModel
import FeedKit

/// Episode notes (full description with clickable links) + actions.
struct EpisodeDetailView: View {
    @Environment(AppModel.self) private var model
    @Environment(\.dismiss) private var dismiss
    let episode: Episode

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(alignment: .leading, spacing: 16) {
                    Text(episode.title)
                        .font(.title3.weight(.semibold))

                    if let date = episode.publicationDate {
                        Text(date, style: .date)
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }

                    Button {
                        Task { await model.play(episode); dismiss() }
                    } label: {
                        Label("Lire", systemImage: "play.fill")
                            .frame(maxWidth: .infinity)
                    }
                    .buttonStyle(.borderedProminent)
                    .disabled(episode.enclosureURL == nil)

                    if let notes = episode.contentHTML ?? episode.summary, !notes.isEmpty {
                        Text(HTMLText.attributed(notes))
                            .font(.callout)
                            .tint(.accentColor)
                    }
                }
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding()
            }
            .navigationTitle("Épisode")
            .inlineNavigationTitle()
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Fermer") { dismiss() }
                }
                if let link = episode.websiteURL ?? episode.enclosureURL {
                    ToolbarItem(placement: .primaryAction) {
                        ShareLink(item: link)
                    }
                }
            }
        }
    }
}
+16 −0
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ struct EpisodeListRow: View {
    @Environment(DownloadController.self) private var downloads
    @Environment(PlaybackController.self) private var playback
    let episode: Episode
    @State private var showingNotes = false

    var body: some View {
        HStack(spacing: 12) {
@@ -39,6 +40,9 @@ struct EpisodeListRow: View {
        }
        .padding(.vertical, 2)
        .contextMenu { contextActions }
        .sheet(isPresented: $showingNotes) {
            EpisodeDetailView(episode: episode)
        }
    }

    private var isCurrent: Bool {
@@ -91,6 +95,18 @@ struct EpisodeListRow: View {
    }

    @ViewBuilder private var contextActions: some View {
        Button {
            showingNotes = true
        } label: {
            Label("Détails / Notes", systemImage: "info.circle")
        }

        if let link = episode.websiteURL ?? episode.enclosureURL {
            ShareLink(item: link) {
                Label("Partager", systemImage: "square.and.arrow.up")
            }
        }

        if model.isQueued(episode) {
            Button {
                Task { await model.dequeue(episode) }
+30 −0
Original line number Diff line number Diff line
import SwiftUI

/// Recent episodes across all subscriptions.
struct InboxView: View {
    @Environment(AppModel.self) private var model

    var body: some View {
        Group {
            if model.inbox.isEmpty {
                ContentUnavailableView(
                    "Aucune nouveauté",
                    systemImage: "tray",
                    description: Text("Les épisodes récents de vos abonnements apparaîtront ici. Tirez pour actualiser.")
                )
            } else {
                List {
                    ForEach(model.inbox) { episode in
                        EpisodeListRow(episode: episode)
                    }
                }
            }
        }
        .navigationTitle("Nouveautés")
        .task { await model.loadInbox() }
        .refreshable {
            await model.refreshAllFeeds()
            await model.loadInbox()
        }
    }
}
Loading