Profile now has a real avatar row (AvatarBadge + Upload Photo…/Remove), wired through last commit's presigned-upload backend: pick a file via NSOpenPanel, crop/rotate/zoom in a new AvatarCropperView, upload, point users.update at the result, then best-effort delete whatever attachment the previous avatar pointed to so replacing/removing a photo doesn't leak an orphaned blob in Outline's storage every time. AvatarCropperView builds its on-screen preview and its final exported image from the exact same SwiftUI view composition (just instantiated twice — once for display, once through ImageRenderer) rather than a separately hand-derived set of crop math for a higher resolution — that's deliberate: there's no way to visually verify a second independent set of transform math agrees with what the user actually saw and confirmed without running the app, so making the export WYSIWYG by construction was the safer choice here. Name is now editable too (TextField + Save, users.update name-only). Email is read-only with a note pointing to Outline's web app instead — per instruction, changing it here isn't supported since email is tied to sign-in. SessionStore gained userId (never stored before — needed for every users.update call) and applyUpdatedProfile(_:), so a successful change reflects immediately in the account footer and everywhere else without waiting for the next auth.info refresh.
15 lines
491 B
Swift
15 lines
491 B
Swift
#if os(macOS)
|
|
import AppKit
|
|
|
|
extension NSImage {
|
|
/// `NSImage` has no built-in JPEG encoder (unlike `UIImage`) — routes
|
|
/// through a bitmap representation to get one.
|
|
func jpegData(compressionQuality: CGFloat) -> Data? {
|
|
guard let tiffData = tiffRepresentation, let bitmap = NSBitmapImageRep(data: tiffData) else {
|
|
return nil
|
|
}
|
|
return bitmap.representation(using: .jpeg, properties: [.compressionFactor: compressionQuality])
|
|
}
|
|
}
|
|
#endif
|