fixed file chooser hopefully.
This commit is contained in:
parent
a0db0d0a8e
commit
2a28d6f53d
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
17
src/ios.rs
17
src/ios.rs
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue