fixed file chooser hopefully.

This commit is contained in:
jess 2026-05-09 12:34:23 -07:00
parent a0db0d0a8e
commit 2a28d6f53d
9 changed files with 191 additions and 20 deletions

View File

@ -46,6 +46,9 @@ void viewport_set_library_progress(struct ViewportHandle *handle,
uint32_t current,
uint32_t total);
/// shows the modal coordinator-wait overlay with a UTF-8 message; null pointer clears.
void viewport_set_coordinating_message(struct ViewportHandle *handle, const char *msg);
/// pushes parallel arrays of placeholder titles and track-number tags (0 = unknown) into the sidebar.
void viewport_set_pending_titles(struct ViewportHandle *handle,
const char *const *titles,

View File

@ -32,14 +32,14 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<string>1.0.2</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
<string>iPhoneSimulator</string>
</array>
<key>CFBundleVersion</key>
<string>1.0.1</string>
<string>1.0.2</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSSupportsOpeningDocumentsInPlace</key>

View File

@ -149,13 +149,27 @@ final class LibraryController: NSObject,
}
#endif
/// claims the security-scoped folder URL and pushes the path to the Rust library scanner.
private var pendingURL: URL?
private var coordAttempt: Int = 0
private var coordSession: Int = 0
private var coordWatchdog: Timer?
private var coordCountdown: Timer?
private var activeCoordinator: NSFileCoordinator?
private static let coordRetryWaits = [3, 5, 10]
private static let coordAttemptTimeout: TimeInterval = 12.0
private static let coordInitialMessage = "File Coordinator has to Cache before we can Access the items. This may take a moment, please wait while loading."
private static let coordFailMessage = """
The File Coordinator might need to Cache. If you can, try files that are not downloaded from iCloud. Even with Keep Downloaded, the Cache of the URLs happens at varied intervals. Close the app, close files, open the app again, try again. It will work if you wait until the URLs have had time to Cache. There is nothing I can do to change this; it's not my bug.
"""
/// claims the security-scoped folder URL and kicks off the coordinator-with-retry pipeline.
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
let kind = pendingKind
pendingKind = 0
print("[YrXtals] documentPicker didPick kind=\(kind) urls=\(urls)")
guard let url = urls.first else { return }
guard let h = view?.viewportHandle else {
guard view?.viewportHandle != nil else {
print("[YrXtals] documentPicker: no viewportHandle, dropping pick")
return
}
@ -169,24 +183,110 @@ final class LibraryController: NSObject,
if scoped {
scopedURL = url
}
#if DEBUG
let fm = FileManager.default
var isDir: ObjCBool = false
let exists = fm.fileExists(atPath: url.path, isDirectory: &isDir)
let reachable = (try? url.checkResourceIsReachable()) ?? false
let readable = fm.isReadableFile(atPath: url.path)
print("[YrXtals.dbg] picked path exists=\(exists) isDir=\(isDir.boolValue) reachable=\(reachable) readable=\(readable)")
if let entries = try? fm.contentsOfDirectory(atPath: url.path) {
print("[YrXtals.dbg] picked dir entries=\(entries.count) sample=\(entries.prefix(5))")
} else {
print("[YrXtals.dbg] picked dir contentsOfDirectory failed")
}
#endif
url.path.withCString { cstr in
pendingURL = url
coordAttempt = 0
setCoordinatingMessage(Self.coordInitialMessage)
startCoordinationAttempt()
}
/// pushes a coordinator-status string into the Rust overlay, or clears it on nil.
private func setCoordinatingMessage(_ msg: String?) {
DispatchQueue.main.async { [weak self] in
guard let h = self?.view?.viewportHandle else { return }
if let m = msg {
m.withCString { cstr in
viewport_set_coordinating_message(h, cstr)
}
} else {
viewport_set_coordinating_message(h, nil)
}
}
}
/// runs one NSFileCoordinator attempt with a watchdog timeout that escalates to a retry on timeout.
private func startCoordinationAttempt() {
guard let url = pendingURL else { return }
coordSession += 1
let mySession = coordSession
if coordAttempt > 0 {
setCoordinatingMessage("(\(coordAttempt)/3) Retry — Loading...")
}
coordWatchdog?.invalidate()
coordWatchdog = Timer.scheduledTimer(withTimeInterval: Self.coordAttemptTimeout, repeats: false) { [weak self] _ in
self?.handleAttemptTimeout(session: mySession)
}
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let coordinator = NSFileCoordinator()
self?.activeCoordinator = coordinator
var err: NSError?
coordinator.coordinate(readingItemAt: url, options: [], error: &err) { coordinatedURL in
DispatchQueue.main.async {
guard let self = self else { return }
guard self.coordSession == mySession else { return }
self.coordWatchdog?.invalidate()
self.coordWatchdog = nil
self.activeCoordinator = nil
self.setCoordinatingMessage(nil)
self.pendingURL = nil
self.coordAttempt = 0
guard let h = self.view?.viewportHandle else { return }
coordinatedURL.path.withCString { cstr in
viewport_apply_picked_folder(h, cstr)
}
}
}
if let e = err {
print("[YrXtals] NSFileCoordinator error: \(e.localizedDescription)")
}
}
}
/// fires when the watchdog elapses without a coordinator success; cancels the in-flight call and starts the retry countdown.
private func handleAttemptTimeout(session: Int) {
guard session == coordSession else { return }
activeCoordinator?.cancel()
activeCoordinator = nil
coordAttempt += 1
if coordAttempt > Self.coordRetryWaits.count {
setCoordinatingMessage(nil)
pendingURL = nil
coordAttempt = 0
showCoordFailureAlert()
return
}
let waitSeconds = Self.coordRetryWaits[coordAttempt - 1]
startCountdown(seconds: waitSeconds)
}
/// runs the visible per-second countdown banner before the next coordinator attempt.
private func startCountdown(seconds: Int) {
var remaining = seconds
setCoordinatingMessage("(\(coordAttempt)/3) Retry in \(remaining)s...")
coordCountdown?.invalidate()
coordCountdown = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
guard let self = self else { timer.invalidate(); return }
remaining -= 1
if remaining <= 0 {
timer.invalidate()
self.coordCountdown = nil
self.startCoordinationAttempt()
} else {
self.setCoordinatingMessage("(\(self.coordAttempt)/3) Retry in \(remaining)s...")
}
}
}
/// shows the final-failure alert when all three retries have elapsed without a successful coordinator return.
private func showCoordFailureAlert() {
guard let root = presentationHost ?? topViewController() else { return }
let alert = UIAlertController(title: "Loading Failed", message: Self.coordFailMessage, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
root.present(alert, animated: true)
}
/// clears the pending picker flag after the document picker dismisses without a selection.
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {

View File

@ -1,6 +1,8 @@
import UIKit
import UniformTypeIdentifiers
import AVFoundation
import MediaPlayer
import FileProvider
import Darwin
/// configures audio, stderr capture, and scene routing at launch.
@ -23,6 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
MPMediaLibrary.requestAuthorization { status in
print("[YrXtals] media library authorization: \(status.rawValue)")
}
Self.warmDocumentPicker()
#if DEBUG
Self.dumpStartupDiagnostics()
Self.startNotificationSpy()
@ -30,6 +33,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return true
}
/// nudges the FileProvider XPC chain awake at launch so the first user-triggered picker presentation actually delivers events.
private static func warmDocumentPicker() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
let warm = UIDocumentPickerViewController(forOpeningContentTypes: [.folder, .audio], asCopy: false)
_ = warm.view
}
NSFileProviderManager.getDomainsWithCompletionHandler { _, _ in }
}
#if DEBUG
/// dumps bundle id, sandbox container, entitlements, FileProvider availability, and reachable temp dirs at startup.
private static func dumpStartupDiagnostics() {

View File

@ -144,6 +144,7 @@ TEAM_ID="$(/usr/libexec/PlistBuddy -c "Print :com.apple.developer.team-identifie
case "$(/usr/libexec/PlistBuddy -c "Print :application-identifier" "$ENT")" in
*\*) /usr/libexec/PlistBuddy -c "Set :application-identifier ${TEAM_ID}.${BUNDLE_ID}" "$ENT" ;;
esac
/usr/libexec/PlistBuddy -c "Delete :keychain-access-groups" "$ENT" 2>/dev/null || true
# locates the Apple Distribution identity in the keychain by SHA-matching the profile's certs.
TMPDIR_PROF="$(mktemp -d)"

View File

@ -186,6 +186,23 @@ pub extern "C" fn viewport_set_library_progress(
h.set_library_progress(current, total);
}
/// shows the coordinator-wait overlay with a UTF-8 message; null pointer clears.
#[unsafe(no_mangle)]
pub extern "C" fn viewport_set_coordinating_message(
handle: *mut ViewportHandle,
msg: *const c_char,
) {
let Some(h) = (unsafe { handle.as_mut() }) else {
return;
};
let s = if msg.is_null() {
None
} else {
unsafe { CStr::from_ptr(msg) }.to_str().ok().map(|s| s.to_string())
};
h.set_coordinating_message(s);
}
/// seeds the sidebar with placeholder rows of titles plus track-number tags ahead of export completion.
#[unsafe(no_mangle)]
pub extern "C" fn viewport_set_pending_titles(

View File

@ -36,6 +36,9 @@ pub struct App {
/// running count and total of the iOS library import progress.
pub library_progress: Option<(u32, u32)>,
/// modal status message shown over the UI while waiting on iOS file-coordinator caching.
pub coordinating_message: Option<String>,
}
/// every visualizer toggle, slider value, and DSP parameter the settings panel exposes.
@ -154,6 +157,7 @@ impl App {
next_decode_id: 0,
track_loading: false,
library_progress: None,
coordinating_message: None,
}
}

View File

@ -44,10 +44,38 @@ pub fn view(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
.into()
};
if app.show_settings {
let body = if app.show_settings {
stack![body, settings_overlay(app)].into()
} else {
body
};
if app.coordinating_message.is_some() {
stack![body, coordinating_overlay(app)].into()
} else {
body
}
}
/// dimmed full-screen card showing the file-coordinator wait message while iOS caches the picked URL.
fn coordinating_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
let msg = app.coordinating_message.clone().unwrap_or_default();
let card = container(
container(text(msg).size(16).color(palette::text()))
.padding(24)
.style(panel_style)
.max_width(420.0),
)
.center_x(Length::Fill)
.center_y(Length::Fill)
.style(scrim_style);
card.into()
}
fn scrim_style(_t: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(Color { r: 0.0, g: 0.0, b: 0.0, a: 0.55 })),
..Default::default()
}
}

View File

@ -292,6 +292,12 @@ impl ViewportHandle {
self.needs_redraw = true;
}
/// shows or clears the modal coordinator-wait overlay.
pub fn set_coordinating_message(&mut self, msg: Option<String>) {
self.state.coordinating_message = msg;
self.needs_redraw = true;
}
/// pushes placeholder track rows from the iOS host ahead of export completion.
pub fn set_pending_titles(&mut self, entries: Vec<(String, Option<u32>)>) {
self.state.set_pending_titles(entries);