Files
iOS/Sources/Extensions/NotificationContent/ImageAttachmentViewController.swift
Zac West 5c104f76e9 Multi-server (#1906)
## Summary
Most, but not all, of the changes necessary to support multi-server throughout the app and all its features.

## Screenshots
| Light | Dark |
| ----- | ---- |
| ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 24](https://user-images.githubusercontent.com/74188/143670011-9b9905ac-1b5b-4a82-b9f3-1490465c4ec5.png) | ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 26](https://user-images.githubusercontent.com/74188/143670012-0080230a-8f68-4f34-9691-db9f5e825a83.png) |
| ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 30](https://user-images.githubusercontent.com/74188/143670015-ceeac558-e039-4639-a186-b5001ab418b8.png) | ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 29](https://user-images.githubusercontent.com/74188/143670016-d72bb69d-83f5-4197-a742-59d208467258.png) |
| ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 47](https://user-images.githubusercontent.com/74188/143670021-6c90c40f-c2f1-4a33-aad9-da6626e99d9d.png) | ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 45](https://user-images.githubusercontent.com/74188/143670024-e99de69d-61d8-4e12-be73-a172242806a0.png) |
| ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 53 05](https://user-images.githubusercontent.com/74188/143670033-1a41ac7e-d4d1-458b-974e-2efdaf8e2288.png) | ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 53 03](https://user-images.githubusercontent.com/74188/143670049-baf4db64-64db-4bfb-88cf-4930f9e5661b.png) |
| ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 53 21](https://user-images.githubusercontent.com/74188/143670053-7ec794f1-857c-4ef6-a92a-5318e90ac6b6.png) | ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 53 19](https://user-images.githubusercontent.com/74188/143670056-a6a5207c-3bba-49fc-b5c6-fc6fa8141f9c.png) |

## Any other notes
- Encapsulates all connectivity, token & server-specific knowledge in a Server model object which gets passed around.
- Updates various places throughout the app to know about and use Server rather than accessing said information through non-server-specific methods.
- Visually requests/notes server in places where it's ambiguous. For example, the Open Page widget will gain a subtitle if multiple servers are set up.
- Allows switching which server is shown in the WebViews. Note that this doesn't take into account multi-window support on iPad/macOS yet.

Most things will migrate successfully however adding an additional server causes things like Shortcuts to start erroring requiring you specify which to use in the particular Shortcut.

Future work necessary:
- Model objects currently clobber each other if their identifiers match. For example, both servers having a zone named `home` means one of them wins the fight for which is known to the app.
- Being remotely logged out on any account causes the app to require onboarding again, when instead it should only do that if the last known server is logged out.
2021-11-27 12:33:46 -08:00

181 lines
6.3 KiB
Swift

import MobileCoreServices
import PromiseKit
import Shared
import UIKit
import UserNotifications
import UserNotificationsUI
import WebKit
class ImageAttachmentViewController: UIViewController, NotificationCategory {
let attachmentURL: URL
let needsEndSecurityScoped: Bool
let image: UIImage
let imageData: Data
let imageUTI: CFString
enum ImageViewType {
case imageView(UIImageView)
case webView(WKWebView)
var view: UIView {
switch self {
case let .imageView(imageView): return imageView
case let .webView(webView): return webView
}
}
}
let visibleView: ImageViewType
required init(api: HomeAssistantAPI, notification: UNNotification, attachmentURL: URL?) throws {
guard let attachmentURL = attachmentURL else {
throw ImageAttachmentError.noAttachment
}
self.needsEndSecurityScoped = attachmentURL.startAccessingSecurityScopedResource()
// rather than hard-coding an acceptable list of UTTypes it's probably easier to just try decoding
// https://developer.apple.com/documentation/usernotifications/unnotificationattachment
// has the full list of what is advertised - at time of writing (iOS 14.5) it's jpeg, gif and png
// but iOS 14 also supports webp, so who knows if it'll be added silently or not
do {
let data = try Data(contentsOf: attachmentURL, options: .alwaysMapped)
guard let image = UIImage(data: data) else {
throw ImageAttachmentError.imageDecodeFailure
}
self.image = image
self.imageData = data
if let imageSource = CGImageSourceCreateWithData(data as CFData, nil),
let uti = CGImageSourceGetType(imageSource) {
self.imageUTI = uti
} else {
// can't figure out, just assume JPEG
self.imageUTI = kUTTypeJPEG
}
if UTTypeConformsTo(imageUTI, kUTTypeGIF) {
// use a WebView for gif so we can animate without pulling in a third party library
let config = with(WKWebViewConfiguration()) {
$0.userContentController = with(WKUserContentController()) {
// we can't use `loadHTMLString` with `<img>` inside to do styling because the webview can't get
// the security scoped file if loaded by the service extension so we need to load data directly
$0.addUserScript(WKUserScript(source: """
var style = document.createElement('style');
style.innerHTML = `
img { width: 100%; height: 100%; }
`;
document.head.appendChild(style);
""", injectionTime: .atDocumentEnd, forMainFrameOnly: true))
}
}
self.visibleView = .webView(with(WKWebView(frame: .zero, configuration: config)) {
$0.scrollView.isScrollEnabled = false
$0.isOpaque = false
$0.backgroundColor = .clear
$0.scrollView.backgroundColor = .clear
})
} else {
self.visibleView = .imageView(with(UIImageView()) {
$0.contentMode = .scaleAspectFit
})
}
} catch {
attachmentURL.stopAccessingSecurityScopedResource()
throw error
}
self.attachmentURL = attachmentURL
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if needsEndSecurityScoped {
attachmentURL.stopAccessingSecurityScopedResource()
}
}
enum ImageAttachmentError: Error {
case noAttachment
case notImage
case imageDecodeFailure
}
private var aspectRatioConstraint: NSLayoutConstraint? {
willSet {
aspectRatioConstraint?.isActive = false
}
didSet {
aspectRatioConstraint?.isActive = true
}
}
private var lastAttachmentURL: URL? {
didSet {
oldValue?.stopAccessingSecurityScopedResource()
}
}
func start() -> Promise<Void> {
lastAttachmentURL = attachmentURL
switch visibleView {
case let .webView(webView):
let mime = UTTypeCopyPreferredTagWithClass(imageUTI, kUTTagClassMIMEType)?.takeRetainedValue() as String?
webView.load(
imageData,
mimeType: mime ?? "image/gif",
characterEncodingName: "UTF-8",
baseURL: attachmentURL
)
case let .imageView(imageView):
imageView.image = image
}
aspectRatioConstraint = NSLayoutConstraint.aspectRatioConstraint(on: visibleView.view, size: image.size)
return .value(())
}
override func loadView() {
class UnanimatingView: UIView {
override func layoutSubviews() {
// avoids the image view sizing up from nothing when initially displaying
// since we don't control our own view's expansion, we need to disable animation at our level
UIView.performWithoutAnimation {
super.layoutSubviews()
}
}
}
view = UnanimatingView()
}
override func viewDidLoad() {
super.viewDidLoad()
let subview = visibleView.view
view.addSubview(subview)
subview.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
subview.topAnchor.constraint(equalTo: view.topAnchor),
subview.leadingAnchor.constraint(equalTo: view.leadingAnchor),
subview.trailingAnchor.constraint(equalTo: view.trailingAnchor),
subview.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
var mediaPlayPauseButtonType: UNNotificationContentExtensionMediaPlayPauseButtonType { .none }
var mediaPlayPauseButtonFrame: CGRect?
func mediaPlay() {}
func mediaPause() {}
}