feat(ui): crisp fades further + fix two latent bugs from repo audit#46
Conversation
Tab switch was still reading slightly soft, so shorten every timed fade one more step: tab cross-dissolve 0.15s → 0.1s, launch intro spring 0.25s → 0.2s, Updates progress-hide / install-button cross-fade and What's New reveal/expand 0.2s → 0.15s. Curves (ease-out) and spring physics unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
ConsoleLogger.readHandler: bind a strong `self` *before* locking shutdownLock. The previous order (`self?.lock()` then `guard let self`) had a window where the logger could deallocate after the lock was taken — the deferred `self?.unlock()` would no-op through the now-nil weak reference, leaving the lock held and deadlocking deinit's stopCapturing(), which also locks it. AltBackup operationDidFinish: fire the local completion notification (and log) when the response URL fails to construct, matching the two guards above it. Previously this path returned silently, so a backgrounded user got no result. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
|
Download the artifacts for this pull request (nightly.link): |
|
Builds for this Pull Request are available at |
Screenshots were rendered with .scaledToFit() inside a ZStack whose frame was pre-sized to the metadata aspect ratio. When actual image pixels differed from the metadata ratio, secondarySystemBackground showed through as left/right bars. .scaledToFill() with the existing .clipShape(RoundedRectangle) clips the overflow cleanly. Applies to both the detail carousel (DetailScreenshotView) and the fullscreen preview carousel (PreviewScreenshotView). https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
|
Builds for this Pull Request are available at |
verifyHash loaded the entire IPA into memory via Data(contentsOf:) before hashing. For large apps (hundreds of MB) this spiked resident memory in the middle of an install, on a device that may already be memory-constrained. Use .mappedIfSafe so the file is memory-mapped instead. SHA256 reads the buffer sequentially, so pages are touched once and reclaimed by the kernel under pressure — same hash result, far smaller memory high-water mark. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
The Dolphin special-case rewrites the downloaded app's CFBundleIdentifier so prior installs keep updating in place. The NSDictionary.write(to:) Bool result was discarded, so a write failure silently shipped the app with the wrong bundle identifier and no trace of why in-place updates broke. Check the return and log on failure. The install still proceeds (the app works either way), but the failure is now diagnosable. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
…e-fire BackgroundRefreshAppsOperation.finish(_:) overrides the base to run two side effects after super.finish(): scheduleFinishedRefreshingNotification (which persists a RefreshAttempt row + arms a local notification) and stopListeningForRunningApps. The base finish() is idempotent via its own isFinished guard, but these side effects are not — a second finish call would write a duplicate RefreshAttempt and re-arm the notification cascade. No current code path double-finishes, so this is latent, not an active bug. Add a `guard !self.isFinished` before super.finish (which flips isFinished) so the side effects run exactly once, matching the operation finish contract. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
Three backup paths failed silently: - backupExists(for:) passed an out-error to NSFileCoordinator but never inspected it. A transient coordination failure read as "no backup", hiding a real backup and mislabelling the Restore control. Now logged. - restorePreviousBackup(for:) and exportBackup(for:) returned silently when backupDirectoryURL(for:) was nil (missing app group) — the user tapped a menu action and nothing happened, with no log or UI feedback. Both now log and show a toast. restorePreviousBackup's copy-failure catch also gained a toast so the user sees the failure instead of a dead tap. Per the "never swallow errors" rule: log domain + code, set a visible state. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
|
Builds for this Pull Request are available at |
performBackup was already safe: it copies into a temp directory then atomically swaps with replaceItemAt. restoreBackup had no such protection — it wrote each directory directly into the live container using copyDirectoryContents (remove-then-copy per item). A failure midway left the live Documents/Library/app-group containers in a half-overwritten state with no path back to the original data. Fix: before touching any live directory, snapshot each one into a UUID temp directory in FileManager.temporaryDirectory. If snapshotting fails, we abort before modifying live data. During restore, any failure triggers rollback of all previously applied targets (and the partially-applied current one) using the snapshots. Rollback failures are logged but don't mask the original error. Snapshots are cleaned up on both success and failure. This mirrors the intent of the backup's own temp+swap pattern: the live app data is never left in an inconsistent state. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
|
Builds for this Pull Request are available at |
This reverts commit 315aa8a.
The earlier switch to .scaledToFill() removed the letterbox bars but let the enlarged image overflow its frame — .clipShape on the outer ZStack wasn't cropping the horizontal overflow during scroll, so screenshots bled past the rounded card and "clipped off" the page edges. Give the Image its own .frame(width:height:) + .clipped() so it's cropped tight to the card before the rounded-corner clipShape. Applies to both the detail carousel and the fullscreen preview carousel. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
|
Builds for this Pull Request are available at |
… backupExists backupExists was called on the main thread from SwiftUI body evaluation for every My Apps row. NSFileCoordinator.coordinate(readingItemAt:) is a synchronous blocking IPC call — if AltBackup still holds a write-coordination lock on the backup directory after completing a backup, MiniStore's main thread blocks waiting for the lock, producing the "instant freeze" on restore. Direct FileManager.fileExists is safe here: the backup URL is constructed locally (no symlink/redirect resolution needed), and the directory is only written by AltBackup, which is a separate process. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
|
Builds for this Pull Request are available at |
…t the on-disk one ResignAppOperation writes a sidestore-<id>:// scheme into the installed app's CFBundleURLSchemes so the Open button can launch it. It was building that scheme from the resigned on-disk ALTApplication's bundleIdentifier (app.bundleIdentifier) instead of context.bundleIdentifier — the customized/remapped identifier that MiniStore actually tracks in CoreData and that InstalledApp.openAppURL requests. When the two differ (remapped bundle IDs, e.g. debug builds appending the team ID, or user-customized IDs), the baked scheme no longer matches what Open launches, so the Open button silently fails after install/activate. Restores the AnyApp(from:bundleId:) wrapping present in both upstream SideStore and the fork. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
InstalledAppBannerRepresentable read app.version and app.name live inside updateUIView. InstalledApp is a plain NSManagedObject (not @observable), so those reads register no SwiftUI dependency, and the row's stored inputs (app reference, isActive, now, icon) don't change when only the version changes in place. After a reinstall/reactivate, the @fetchrequest republishes the same objectIDs, ForEach keeps the reference-equal row, and SwiftUI skips updateUIView — so the banner kept showing the stale AltBackup "1.0" placeholder version until the next clock tick. Thread version and name as value-type inputs to the row and representable so a CoreData change breaks reference-equality and forces updateUIView to re-run, mirroring how icon is already passed and the UIKit cell-reconfigure behaved. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
…ctivate fixes - The "_install context ?? matches upstream SideStore" note was factually wrong: both upstream SideStore/SideStore and the fork create the context unconditionally and ignore the parameter. context ?? is a MiniStore-only divergence. Documented the suspected placeholder-failure coupling and left the code as-is under review. - Added gotchas for the two reactivate fixes shipped this session: the Open URL scheme bundle-ID (ResignAppOperation) and the SwiftUI banner version staleness. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
JIT was deliberately removed from MiniStore (EnableJITOperation, EnableJITContext, AppManager.enableJIT, My Apps menu/swipe actions all gone; only inert enum cases remain). The minimuxer primitives (debugApp/attachDebugger) are still present, so it can be re-added from the fork without touching the Rust bridge. Documented for future reference. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
|
Builds for this Pull Request are available at |
… grey band The content card applied .background(altBackground) to its content-sized VStack, then the call site expanded it with .frame(minHeight: geoHeight - 44) AFTER the background. When the card content was short, the expanded bottom region was left transparent and revealed a grey system backdrop instead of the page background — the "grey band with OLED on" in the lower App Detail page. Move the minHeight onto the card's own frame (before .background) so altBackground fills the entire card frame; the top rounded corners still reveal the artwork via the clip shape. altBackground already resolves to pure black in OLED, so the band is now black. https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
1. Crisp fade animations (follow-up)
Tab switching still read slightly soft, so every timed fade dropped one more notch:
TabSwitchFadeAnimator)Curves and spring physics unchanged.
2. Repo audit — two latent bugs fixed
Fanned out a 4-area audit (operations, AltStoreCore, SwiftUI screens, SideStore/Widget/Backup/Shared). Most candidates were false positives or inherited-intentional; two real, safe fixes landed:
ConsoleLogger.readHandler— bind a strongselfbefore lockingshutdownLock. The old order had a window where the logger could deallocate after locking; the deferredself?.unlock()then no-ops through the nil weak ref, leaving the lock held and deadlockingdeinit'sstopCapturing().AltBackupoperationDidFinish— fire the local completion notification + log when the response URL fails to construct, matching the two guards above it. Previously returned silently → a backgrounded user got no result.Audited and deliberately NOT changed (verified on disk)
appcorrectly before the dismiss-binding nils it.AppManager.bundleIdentifier—performAndWaitalways runs the block; no nil-unwrap.DatabaseManagerorphan-deletesourceURL INpredicate — CoreData URL predicates work (same pattern inLaunchViewController).allAppsExpired/ first-vs-last expiring — inherited AltStore timeline-relevance design, intentional.Source17To17_1migration,BackgroundRefreshAppsOperationUnmanaged keep-alive — regression-sensitive / already-guarded; not touched blind.Shared/Connections/XPCConnection— dead code (never instantiated).Notes
Animation timing is best judged on-device. No logic touched beyond the two audit fixes.
🤖 Generated with Claude Code
https://claude.ai/code/session_017rnLQsvspk1uZDExv3rUxx
Generated by Claude Code