frenzy-ios/Reader/Screens/Read/ReadViewController.swift

240 lines
8.4 KiB
Swift
Raw Normal View History

2022-01-10 04:38:44 +00:00
//
// ReadViewController.swift
// Reader
//
// Created by Shadowfacts on 1/9/22.
//
import UIKit
import WebKit
import HTMLEntities
2022-01-11 03:34:29 +00:00
import SafariServices
2022-01-10 04:38:44 +00:00
class ReadViewController: UIViewController {
private static let publishedFormatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
f.timeStyle = .medium
return f
}()
let fervorController: FervorController
let item: Item
2022-01-15 19:49:29 +00:00
#if targetEnvironment(macCatalyst)
private var itemReadObservation: NSKeyValueObservation?
#endif
2022-01-10 04:38:44 +00:00
override var prefersStatusBarHidden: Bool {
navigationController?.isNavigationBarHidden ?? false
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
.slide
}
init(item: Item, fervorController: FervorController) {
self.fervorController = fervorController
self.item = item
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.largeTitleDisplayMode = .never
view.backgroundColor = .appBackground
2022-01-12 16:36:12 +00:00
view.addInteraction(StretchyMenuInteraction(delegate: self))
2022-01-10 04:38:44 +00:00
let webView = WKWebView()
webView.translatesAutoresizingMaskIntoConstraints = false
webView.navigationDelegate = self
2022-01-11 03:34:29 +00:00
webView.uiDelegate = self
// transparent background required to prevent white flash in dark mode, just using .appBackground doesn't work
webView.isOpaque = false
webView.backgroundColor = .clear
2022-01-10 04:38:44 +00:00
if let content = itemContentHTML() {
webView.loadHTMLString(content, baseURL: item.url)
2022-01-10 04:38:44 +00:00
}
view.addSubview(webView)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
2022-01-15 19:49:29 +00:00
if let url = item.url {
activityItemsConfiguration = UIActivityItemsConfiguration(objects: [url as NSURL])
}
#if targetEnvironment(macCatalyst)
itemReadObservation = item.observe(\.read) { [unowned self] _, _ in
self.updateToggleReadToolbarImage()
}
#endif
2022-01-10 04:38:44 +00:00
}
private static let css = try! String(contentsOf: Bundle.main.url(forResource: "read", withExtension: "css")!)
2022-01-14 21:26:28 +00:00
private static let js = try! String(contentsOf: Bundle.main.url(forResource: "read", withExtension: "js")!)
2022-01-10 04:38:44 +00:00
private func itemContentHTML() -> String? {
guard let content = item.content else {
return nil
}
var info = ""
if let title = item.title, !title.isEmpty {
info += "<h1 id=\"item-title\">"
if let url = item.url {
info += "<a href=\"\(url.absoluteString)\">"
}
info += title.htmlEscape()
if item.url != nil {
info += "</a>"
}
info += "</h1>"
}
if let feedTitle = item.feed!.title, !feedTitle.isEmpty {
info += "<h2 id=\"item-feed-title\">\(feedTitle.htmlEscape())</h2>"
}
if let author = item.author, !author.isEmpty {
info += "<h3 id=\"item-author\">\(author)</h3>"
}
if let published = item.published {
let formatted = ReadViewController.publishedFormatter.string(from: published)
info += "<h3 id=\"item-published\">\(formatted)</h3>"
}
return """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
<style>\(ReadViewController.css)</style>
2022-01-14 21:26:28 +00:00
<script async>\(ReadViewController.js)</script>
2022-01-10 04:38:44 +00:00
</head>
<body>
<div id="item-info">
\(info)
</div>
<div id="item-content">
\(content)
</div>
</body>
</html>
"""
}
2022-01-12 15:55:50 +00:00
private func createSafariVC(url: URL) -> SFSafariViewController {
let vc = SFSafariViewController(url: url)
vc.preferredControlTintColor = .appTintColor
return vc
}
2022-01-15 19:49:29 +00:00
#if targetEnvironment(macCatalyst)
@objc func toggleItemRead(_ item: NSToolbarItem) {
Task {
await fervorController.markItem(self.item, read: !self.item.read)
updateToggleReadToolbarImage()
}
}
private func updateToggleReadToolbarImage() {
guard let titlebar = view.window?.windowScene?.titlebar,
let item = titlebar.toolbar?.items.first(where: { $0.itemIdentifier == .toggleItemRead }) else {
return
}
item.image = UIImage(systemName: self.item.read ? "checkmark.circle.fill" : "checkmark.circle")
}
#endif
2022-01-10 04:38:44 +00:00
}
extension ReadViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
2022-01-12 16:26:24 +00:00
if navigationAction.navigationType == .linkActivated {
let url = navigationAction.request.url!
if url.fragment != nil {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
components.fragment = nil
if components.url == item.url {
return .allow
}
}
2022-01-12 15:55:50 +00:00
present(createSafariVC(url: url), animated: true)
2022-01-11 03:34:29 +00:00
return .cancel
2022-01-12 16:26:24 +00:00
} else {
return .allow
2022-01-11 03:34:29 +00:00
}
}
}
extension ReadViewController: WKUIDelegate {
func webView(_ webView: WKWebView, contextMenuConfigurationFor elementInfo: WKContextMenuElementInfo) async -> UIContextMenuConfiguration? {
guard let url = elementInfo.linkURL,
["http", "https"].contains(url.scheme?.lowercased()) else {
return nil
}
2022-01-12 15:55:50 +00:00
return UIContextMenuConfiguration(identifier: nil) { [unowned self] in
self.createSafariVC(url: url)
2022-01-11 03:34:29 +00:00
} actionProvider: { _ in
return UIMenu(children: [
UIAction(title: "Open in Safari", image: UIImage(systemName: "safari"), handler: { [weak self] _ in
2022-01-12 17:56:02 +00:00
guard let self = self else { return }
self.present(self.createSafariVC(url: url), animated: true)
2022-01-11 03:34:29 +00:00
}),
UIAction(title: "Share", image: UIImage(systemName: "square.and.arrow.up"), handler: { [weak self] _ in
self?.present(UIActivityViewController(activityItems: [url], applicationActivities: nil), animated: true)
}),
])
}
}
func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) {
if let vc = animator.previewViewController as? SFSafariViewController {
animator.preferredCommitStyle = .pop
animator.addCompletion {
self.present(vc, animated: true)
}
2022-01-10 04:38:44 +00:00
}
}
}
2022-01-12 16:36:12 +00:00
extension ReadViewController: StretchyMenuInteractionDelegate {
func stretchyMenuTitle() -> String? {
return nil
}
func stretchyMenuItems() -> [StretchyMenuItem] {
guard let url = item.url else {
return []
}
2022-01-15 19:49:29 +00:00
var items = [
2022-01-12 16:36:12 +00:00
StretchyMenuItem(title: "Open in Safari", subtitle: nil, action: { [unowned self] in
self.present(createSafariVC(url: url), animated: true)
}),
StretchyMenuItem(title: item.read ? "Mark as Unread" : "Mark as Read", subtitle: nil, action: { [unowned self] in
Task {
await self.fervorController.markItem(item, read: !item.read)
}
2022-01-12 16:36:12 +00:00
}),
]
2022-01-15 19:49:29 +00:00
#if !targetEnvironment(macCatalyst)
items.insert(StretchyMenuItem(title: "Share", subtitle: nil, action: { [unowned self] in
self.present(UIActivityViewController(activityItems: [url], applicationActivities: nil), animated: true)
}), at: 1)
#endif
return items
2022-01-12 16:36:12 +00:00
}
}