Add read view
This commit is contained in:
parent
dab4d6075a
commit
f53f198071
|
@ -44,6 +44,9 @@
|
|||
D6E24367278BA2660005E546 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D6E24366278BA2660005E546 /* SwiftSoup */; };
|
||||
D6E24369278BABB40005E546 /* UIColor+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E24368278BABB40005E546 /* UIColor+App.swift */; };
|
||||
D6E2436B278BB1880005E546 /* HomeCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E2436A278BB1880005E546 /* HomeCollectionViewCell.swift */; };
|
||||
D6E2436E278BD8160005E546 /* ReadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E2436D278BD8160005E546 /* ReadViewController.swift */; };
|
||||
D6E24371278BE1250005E546 /* HTMLEntities in Frameworks */ = {isa = PBXBuildFile; productRef = D6E24370278BE1250005E546 /* HTMLEntities */; };
|
||||
D6E24373278BE2B80005E546 /* read.css in Resources */ = {isa = PBXBuildFile; fileRef = D6E24372278BE2B80005E546 /* read.css */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -124,6 +127,8 @@
|
|||
D6E24361278BA1410005E546 /* ItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D6E24368278BABB40005E546 /* UIColor+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+App.swift"; sourceTree = "<group>"; };
|
||||
D6E2436A278BB1880005E546 /* HomeCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D6E2436D278BD8160005E546 /* ReadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadViewController.swift; sourceTree = "<group>"; };
|
||||
D6E24372278BE2B80005E546 /* read.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = read.css; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -133,6 +138,7 @@
|
|||
files = (
|
||||
D6C68829272CD2BA00874C10 /* Fervor.framework in Frameworks */,
|
||||
D6E24367278BA2660005E546 /* SwiftSoup in Frameworks */,
|
||||
D6E24371278BE1250005E546 /* HTMLEntities in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -166,6 +172,7 @@
|
|||
D65B18BF2750533E004A9448 /* Home */,
|
||||
D65B18B027504691004A9448 /* Login */,
|
||||
D6E2434A278B455C0005E546 /* Items */,
|
||||
D6E2436C278BD80B0005E546 /* Read */,
|
||||
);
|
||||
path = Screens;
|
||||
sourceTree = "<group>";
|
||||
|
@ -237,6 +244,7 @@
|
|||
D6C687F7272CD27700874C10 /* Assets.xcassets */,
|
||||
D6C687F9272CD27700874C10 /* LaunchScreen.storyboard */,
|
||||
D6C687FC272CD27700874C10 /* Info.plist */,
|
||||
D6E24372278BE2B80005E546 /* read.css */,
|
||||
);
|
||||
path = Reader;
|
||||
sourceTree = "<group>";
|
||||
|
@ -285,6 +293,14 @@
|
|||
path = Items;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6E2436C278BD80B0005E546 /* Read */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6E2436D278BD8160005E546 /* ReadViewController.swift */,
|
||||
);
|
||||
path = Read;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
|
@ -316,6 +332,7 @@
|
|||
name = Reader;
|
||||
packageProductDependencies = (
|
||||
D6E24366278BA2660005E546 /* SwiftSoup */,
|
||||
D6E24370278BE1250005E546 /* HTMLEntities */,
|
||||
);
|
||||
productName = Reader;
|
||||
productReference = D6C687E8272CD27600874C10 /* Reader.app */;
|
||||
|
@ -413,6 +430,7 @@
|
|||
mainGroup = D6C687DF272CD27600874C10;
|
||||
packageReferences = (
|
||||
D6E24365278BA2660005E546 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||
D6E2436F278BE1250005E546 /* XCRemoteSwiftPackageReference "swift-html-entities" */,
|
||||
);
|
||||
productRefGroup = D6C687E9272CD27600874C10 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -433,6 +451,7 @@
|
|||
files = (
|
||||
D6C687FB272CD27700874C10 /* LaunchScreen.storyboard in Resources */,
|
||||
D6C687F8272CD27700874C10 /* Assets.xcassets in Resources */,
|
||||
D6E24373278BE2B80005E546 /* read.css in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -480,6 +499,7 @@
|
|||
D65B18BE275051A1004A9448 /* LocalData.swift in Sources */,
|
||||
D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */,
|
||||
D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */,
|
||||
D6E2436E278BD8160005E546 /* ReadViewController.swift in Sources */,
|
||||
D65B18C127505348004A9448 /* HomeViewController.swift in Sources */,
|
||||
D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
|
@ -925,6 +945,14 @@
|
|||
minimumVersion = 2.3.0;
|
||||
};
|
||||
};
|
||||
D6E2436F278BE1250005E546 /* XCRemoteSwiftPackageReference "swift-html-entities" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Kitura/swift-html-entities.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.0.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
@ -933,6 +961,11 @@
|
|||
package = D6E24365278BA2660005E546 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
productName = SwiftSoup;
|
||||
};
|
||||
D6E24370278BE1250005E546 /* HTMLEntities */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D6E2436F278BE1250005E546 /* XCRemoteSwiftPackageReference "swift-html-entities" */;
|
||||
productName = HTMLEntities;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
|
|
@ -206,7 +206,7 @@ extension HomeViewController: UICollectionViewDelegate {
|
|||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
let vc = ItemsViewController(fervorController: fervorController, fetchRequest: item.fetchRequest)
|
||||
let vc = ItemsViewController(fetchRequest: item.fetchRequest, fervorController: fervorController)
|
||||
vc.title = item.title
|
||||
show(vc, sender: nil)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class ItemsViewController: UIViewController {
|
|||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
private var resultsController: NSFetchedResultsController<Item>!
|
||||
|
||||
init(fervorController: FervorController, fetchRequest: NSFetchRequest<Item>) {
|
||||
init(fetchRequest: NSFetchRequest<Item>, fervorController: FervorController) {
|
||||
self.fervorController = fervorController
|
||||
self.fetchRequest = fetchRequest
|
||||
|
||||
|
@ -35,6 +35,7 @@ class ItemsViewController: UIViewController {
|
|||
configuration.backgroundColor = .appBackground
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||
collectionView.delegate = self
|
||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
collectionView.register(ItemCollectionViewCell.self, forCellWithReuseIdentifier: "itemCell")
|
||||
view.addSubview(collectionView)
|
||||
|
@ -78,3 +79,13 @@ extension ItemsViewController: NSFetchedResultsControllerDelegate {
|
|||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension ItemsViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
let vc = ReadViewController(item: item, fervorController: fervorController)
|
||||
show(vc, sender: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
//
|
||||
// ReadViewController.swift
|
||||
// Reader
|
||||
//
|
||||
// Created by Shadowfacts on 1/9/22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import WebKit
|
||||
import HTMLEntities
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
let webView = WKWebView()
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
webView.navigationDelegate = self
|
||||
if let content = itemContentHTML() {
|
||||
// todo: using the bundle url is the only way to get the stylesheet to load, but feels wrong
|
||||
// will break, e.g., images with relative urls
|
||||
webView.loadHTMLString(content, baseURL: Bundle.main.bundleURL)
|
||||
}
|
||||
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),
|
||||
])
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
navigationController?.hidesBarsOnSwipe = true
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
navigationController?.hidesBarsOnSwipe = false
|
||||
}
|
||||
|
||||
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" />
|
||||
<link rel="stylesheet" href="\(Bundle.main.url(forResource: "read", withExtension: "css")!.absoluteString)" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="item-info">
|
||||
\(info)
|
||||
</div>
|
||||
<div id="item-content">
|
||||
\(content)
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReadViewController: WKNavigationDelegate {
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
|
||||
let url = navigationAction.request.url!
|
||||
if url == Bundle.main.bundleURL {
|
||||
return .allow
|
||||
}
|
||||
return .cancel
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ extension UIColor {
|
|||
static let appBackground = UIColor { traitCollection in
|
||||
switch traitCollection.userInterfaceStyle {
|
||||
case .dark:
|
||||
return UIColor(white: 0.1, alpha: 1)
|
||||
return UIColor(red: 25/255, green: 25/255, blue: 25/255, alpha: 1)
|
||||
case .unspecified, .light:
|
||||
fallthrough
|
||||
@unknown default:
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
--tint-color: rgb(255, 59, 48); /* .systemRed */
|
||||
--dark-tint-color: rgb(255, 69, 58); /* .systemRed */
|
||||
--secondary-text-color: rgb(85, 85, 85); /* .darkGray */
|
||||
--dark-secondary-text-color: rgb(170, 170, 170); /* .lightGray */
|
||||
--background-color: white; /* .appBackground */
|
||||
--dark-background-color: rgb(25, 25, 25); /* .appBackground */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 12px;
|
||||
font-family: ui-serif;
|
||||
overflow-wrap: break-word;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--tint-color);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
color: var(--secondary-text-color);
|
||||
font-style: italic;
|
||||
font-family: ui-sans-serif;
|
||||
font-size: 12pt;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: ui-monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-wrap: normal;
|
||||
overflow-x: auto;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
#item-info {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#item-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#item-title a {
|
||||
text-decoration: none;
|
||||
color: text;
|
||||
}
|
||||
|
||||
#item-feed-title,
|
||||
#item-author,
|
||||
#item-published {
|
||||
margin: 0.25em 0;
|
||||
font-family: ui-sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#item-feed-title {
|
||||
color: var(--tint-color);
|
||||
}
|
||||
|
||||
#item-author,
|
||||
#item-published {
|
||||
font-style: italic;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
#item-content {
|
||||
font-size: 14pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--tint-color: var(--dark-tint-color);
|
||||
--secondary-text-color: var(--dark-secondary-text-color);
|
||||
--background-color: var(--dark-background-color);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue