Commit 4c351744 authored by Kourser's avatar Kourser
Browse files

Subscriptions, playback, queue, downloads + multiplatform (iOS/iPadOS/macOS)



Builds out the MVP on top of the scaffold:

- StorageKit (GRDB/SQLite): subscribe-by-URL with upsert/dedup, play queue
  (reorder + auto-advance), offline download records. Cascade deletes.
- FeedKit: networked FeedFetcher (injectable loader) over the RSS parser.
- PlaybackKit: AudioPlayerEngine protocol (+ AVPlayer engine) and an
  observable PlaybackController; lock-screen via MPNowPlayingInfoCenter /
  MPRemoteCommandCenter; background audio; variable speed.
- App: DownloadController, AppModel orchestration, NavigationSplitView root
  (sidebar + detail, collapses to a stack on iPhone), mini player, queue and
  podcast-detail screens.
- Multiplatform native target (SDKROOT=auto, adds macOS 15; not Catalyst) with
  a sandbox + network-client entitlement on macOS; iOS-only modifiers guarded.

29 package tests green; iOS and macOS both build; runtime-verified in the
iPhone and iPad simulators.

Co-Authored-By: default avatarClaude <claude@anthropic.com>
parent 260e318f
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -13,3 +13,6 @@ xcuserdata/
.build/
.swiftpm/
Packages/*/.build/

# Local xcodebuild SPM clone cache
.build-spm/
+10 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>
+58 −8
Original line number Diff line number Diff line
@@ -6,6 +6,13 @@
	objectVersion = 77;
	objects = {

/* Begin PBXBuildFile section */
		ABCDEF0123456789ABCD0014 /* PodcastModel in Frameworks */ = {isa = PBXBuildFile; productRef = ABCDEF0123456789ABCD0011 /* PodcastModel */; };
		ABCDEF0123456789ABCD0015 /* FeedKit in Frameworks */ = {isa = PBXBuildFile; productRef = ABCDEF0123456789ABCD0012 /* FeedKit */; };
		ABCDEF0123456789ABCD0016 /* StorageKit in Frameworks */ = {isa = PBXBuildFile; productRef = ABCDEF0123456789ABCD0013 /* StorageKit */; };
		ABCDEF0123456789ABCD0018 /* PlaybackKit in Frameworks */ = {isa = PBXBuildFile; productRef = ABCDEF0123456789ABCD0017 /* PlaybackKit */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
		ABCDEF0123456789ABCD0004 /* 2ndPod.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = 2ndPod.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@@ -23,6 +30,10 @@
			isa = PBXFrameworksBuildPhase;
			buildActionMask = 2147483647;
			files = (
				ABCDEF0123456789ABCD0014 /* PodcastModel in Frameworks */,
				ABCDEF0123456789ABCD0015 /* FeedKit in Frameworks */,
				ABCDEF0123456789ABCD0016 /* StorageKit in Frameworks */,
				ABCDEF0123456789ABCD0018 /* PlaybackKit in Frameworks */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
@@ -65,6 +76,10 @@
			);
			name = 2ndPod;
			packageProductDependencies = (
				ABCDEF0123456789ABCD0011 /* PodcastModel */,
				ABCDEF0123456789ABCD0012 /* FeedKit */,
				ABCDEF0123456789ABCD0013 /* StorageKit */,
				ABCDEF0123456789ABCD0017 /* PlaybackKit */,
			);
			productName = 2ndPod;
			productReference = ABCDEF0123456789ABCD0004 /* 2ndPod.app */;
@@ -94,6 +109,9 @@
			);
			mainGroup = ABCDEF0123456789ABCD0002;
			minimizedProjectReferenceProxies = 1;
			packageReferences = (
				ABCDEF0123456789ABCD0010 /* XCLocalSwiftPackageReference "Packages/Core" */,
			);
			preferredProjectObjectVersion = 77;
			productRefGroup = ABCDEF0123456789ABCD0003 /* Products */;
			projectDirPath = "";
@@ -216,24 +234,27 @@
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
				"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = 2ndPod.macOS.entitlements;
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = 1;
				DEVELOPMENT_TEAM = 88NB46UUP7;
				ENABLE_PREVIEWS = YES;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_FILE = Info.plist;
				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
				INFOPLIST_FILE = Info.plist;
				IPHONEOS_DEPLOYMENT_TARGET = 18.0;
				LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
				"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
				MACOSX_DEPLOYMENT_TARGET = 15.0;
				MARKETING_VERSION = 1.0;
				PRODUCT_BUNDLE_IDENTIFIER = "com.secondpod.SecondPod";
				PRODUCT_BUNDLE_IDENTIFIER = com.secondpod.SecondPod;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SDKROOT = iphoneos;
				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
				SDKROOT = auto;
				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
				SWIFT_EMIT_LOC_STRINGS = YES;
				SWIFT_VERSION = 5.0;
				TARGETED_DEVICE_FAMILY = "1,2";
@@ -245,24 +266,27 @@
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
				"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = 2ndPod.macOS.entitlements;
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = 1;
				DEVELOPMENT_TEAM = 88NB46UUP7;
				ENABLE_PREVIEWS = YES;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_FILE = Info.plist;
				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
				INFOPLIST_FILE = Info.plist;
				IPHONEOS_DEPLOYMENT_TARGET = 18.0;
				LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
				"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
				MACOSX_DEPLOYMENT_TARGET = 15.0;
				MARKETING_VERSION = 1.0;
				PRODUCT_BUNDLE_IDENTIFIER = "com.secondpod.SecondPod";
				PRODUCT_BUNDLE_IDENTIFIER = com.secondpod.SecondPod;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SDKROOT = iphoneos;
				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
				SDKROOT = auto;
				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
				SWIFT_EMIT_LOC_STRINGS = YES;
				SWIFT_VERSION = 5.0;
				TARGETED_DEVICE_FAMILY = "1,2";
@@ -291,6 +315,32 @@
			defaultConfigurationName = Release;
		};
/* End XCConfigurationList section */

/* Begin XCLocalSwiftPackageReference section */
		ABCDEF0123456789ABCD0010 /* XCLocalSwiftPackageReference "Packages/Core" */ = {
			isa = XCLocalSwiftPackageReference;
			relativePath = Packages/Core;
		};
/* End XCLocalSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
		ABCDEF0123456789ABCD0011 /* PodcastModel */ = {
			isa = XCSwiftPackageProductDependency;
			productName = PodcastModel;
		};
		ABCDEF0123456789ABCD0012 /* FeedKit */ = {
			isa = XCSwiftPackageProductDependency;
			productName = FeedKit;
		};
		ABCDEF0123456789ABCD0013 /* StorageKit */ = {
			isa = XCSwiftPackageProductDependency;
			productName = StorageKit;
		};
		ABCDEF0123456789ABCD0017 /* PlaybackKit */ = {
			isa = XCSwiftPackageProductDependency;
			productName = PlaybackKit;
		};
/* End XCSwiftPackageProductDependency section */
	};
	rootObject = ABCDEF0123456789ABCD0001 /* Project object */;
}
+15 −0
Original line number Diff line number Diff line
{
  "originHash" : "9c03783b50521d6c0992c7af1bb6617ee9a728efa603264c3287df0acba6ccd1",
  "pins" : [
    {
      "identity" : "grdb.swift",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/groue/GRDB.swift",
      "state" : {
        "revision" : "b83108d10f42680d78f23fe4d4d80fc88dab3212",
        "version" : "7.11.1"
      }
    }
  ],
  "version" : 3
}
+48 −0
Original line number Diff line number Diff line
import SwiftUI

struct AddPodcastView: View {
    @Environment(AppModel.self) private var model
    @Environment(\.dismiss) private var dismiss
    @State private var urlString = ""

    var body: some View {
        NavigationStack {
            Form {
                Section("URL du flux RSS") {
                    TextField("https://exemple.com/feed.xml", text: $urlString)
                        .urlFieldStyle()
                        .submitLabel(.done)
                        .onSubmit(add)
                }
                if let error = model.errorMessage {
                    Section {
                        Text(error).foregroundStyle(.red)
                    }
                }
            }
            .navigationTitle("Ajouter un podcast")
            .inlineNavigationTitle()
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Annuler") { dismiss() }
                }
                ToolbarItem(placement: .confirmationAction) {
                    if model.isAdding {
                        ProgressView()
                    } else {
                        Button("Ajouter", action: add)
                            .disabled(urlString.trimmingCharacters(in: .whitespaces).isEmpty)
                    }
                }
            }
        }
    }

    private func add() {
        Task {
            if await model.subscribe(urlString: urlString) {
                dismiss()
            }
        }
    }
}
Loading