// // AdminUIKit.swift // FTStore Admin // // The three UIKit helpers FTStore Admin borrows from FTStore's engine, inlined // so this app needs no Swift package at all: it links nothing but the system // frameworks, which is what keeps a decrypted IPA down to a login form. // // Adapted from NimbleKit (`manager/NimbleKit`, MIT — Copyright 2025 CLARATION): // `UIImage+resize.swift` and `UlActivityViewController+Present.swift`. The // presenter lookup is rewritten against `UIWindowScene`, since the original // went through the long-deprecated `UIApplication.keyWindow`. // import UIKit import AVFoundation // MARK: - Presentation extension UIApplication { /// The view controller a modal should be presented from: the foreground /// scene's key window, walked down through navigation/tab/presented /// controllers. /// /// The starting point is resolved in the body rather than as a default /// argument — a default argument is evaluated at the call site, which is not /// main-actor isolated, and reading `foregroundKeyWindow` there doesn't /// compile. @MainActor static func topViewController() -> UIViewController? { _topViewController(from: shared.foregroundKeyWindow?.rootViewController) } @MainActor private static func _topViewController(from controller: UIViewController?) -> UIViewController? { if let navigation = controller as? UINavigationController { return _topViewController(from: navigation.visibleViewController) } if let tab = controller as? UITabBarController, let selected = tab.selectedViewController { return _topViewController(from: selected) } if let presented = controller?.presentedViewController { return _topViewController(from: presented) } return controller } @MainActor var foregroundKeyWindow: UIWindow? { connectedScenes .compactMap { $0 as? UIWindowScene } .first { $0.activationState == .foregroundActive }? .windows .first { $0.isKeyWindow } ?? connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap(\.windows) .first { $0.isKeyWindow } } } extension UIActivityViewController { /// Present a share sheet from the top-most controller. Used for the settings /// backup export, which is a file the operator hands off to somewhere else. @MainActor static func show(activityItems: [Any], applicationActivities: [UIActivity]? = nil) { guard let presenter = UIApplication.topViewController() else { return } let controller = Self( activityItems: activityItems, applicationActivities: applicationActivities ) // iPad presents this as a popover and crashes without an anchor. if let popover = controller.popoverPresentationController { popover.sourceView = presenter.view popover.sourceRect = CGRect( x: presenter.view.bounds.midX, y: presenter.view.bounds.midY, width: 0, height: 0 ) popover.permittedArrowDirections = [] } presenter.present(controller, animated: true) } } // MARK: - Images extension UIImage { /// Centre-crop to a square, so a tweak icon picked from Photos or the camera /// isn't letterboxed when the server scales it. func resizeToSquare() -> UIImage? { let side = min(size.width, size.height) let origin = CGPoint(x: (size.width - side) / 2, y: (size.height - side) / 2) UIGraphicsBeginImageContextWithOptions(CGSize(width: side, height: side), false, scale) draw(at: CGPoint(x: -origin.x, y: -origin.y)) let square = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return square } /// Aspect-fit into `width` × `height` at scale 1, so the uploaded icon is the /// pixel size the caller asked for rather than a Retina multiple of it. func resize(_ width: Int, _ height: Int) -> UIImage { let target = AVFoundation.AVMakeRect( aspectRatio: size, insideRect: CGRect(origin: .zero, size: CGSize(width: width, height: height)) ).size let format = UIGraphicsImageRendererFormat() format.scale = 1 return UIGraphicsImageRenderer(size: target, format: format).image { _ in draw(in: CGRect(origin: .zero, size: target)) } } }