Files
Felitendo e0b2692e83 docs(admin): call it FTStore Admin, not "the console"
The app is FTStore Admin. Its own AGENTS.md already sets the vocabulary — "the
FTStore Admin server" for web/, "the FTStore Admin app" for this one — and then
most of the comments in it said "the console" anyway.

Three user-visible strings went with it: the lock screen, the menu item that
locks it, and the biometric prompt. The storage-notification footer named the
console too. Their catalog keys move with them, in every locale the app ships.

Not "the app": inside these files that already means FTStore on somebody's
phone, so a blind swap produced lines like "Dashboard and app only, never the
app".

AdminConsoleView is AdminHomeView. The Console/ directory keeps its name: the
history of Console/*.swift runs back through manager/FTStore/Admin/ and is
documented as doing so, which is worth more than a path nobody reads as a name.

Also fills a translation gap this turned up — the status-change footer in the
feedback detail was untranslated.
2026-08-10 17:54:46 +02:00

123 lines
4.0 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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))
}
}
}