Add read view

This commit is contained in:
Shadowfacts 2022-01-09 23:38:44 -05:00
parent dab4d6075a
commit f53f198071
6 changed files with 276 additions and 3 deletions

View File

@ -44,6 +44,9 @@
D6E24367278BA2660005E546 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D6E24366278BA2660005E546 /* SwiftSoup */; }; D6E24367278BA2660005E546 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D6E24366278BA2660005E546 /* SwiftSoup */; };
D6E24369278BABB40005E546 /* UIColor+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E24368278BABB40005E546 /* UIColor+App.swift */; }; D6E24369278BABB40005E546 /* UIColor+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E24368278BABB40005E546 /* UIColor+App.swift */; };
D6E2436B278BB1880005E546 /* HomeCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E2436A278BB1880005E546 /* HomeCollectionViewCell.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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -124,6 +127,8 @@
D6E24361278BA1410005E546 /* ItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCollectionViewCell.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -133,6 +138,7 @@
files = ( files = (
D6C68829272CD2BA00874C10 /* Fervor.framework in Frameworks */, D6C68829272CD2BA00874C10 /* Fervor.framework in Frameworks */,
D6E24367278BA2660005E546 /* SwiftSoup in Frameworks */, D6E24367278BA2660005E546 /* SwiftSoup in Frameworks */,
D6E24371278BE1250005E546 /* HTMLEntities in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -166,6 +172,7 @@
D65B18BF2750533E004A9448 /* Home */, D65B18BF2750533E004A9448 /* Home */,
D65B18B027504691004A9448 /* Login */, D65B18B027504691004A9448 /* Login */,
D6E2434A278B455C0005E546 /* Items */, D6E2434A278B455C0005E546 /* Items */,
D6E2436C278BD80B0005E546 /* Read */,
); );
path = Screens; path = Screens;
sourceTree = "<group>"; sourceTree = "<group>";
@ -237,6 +244,7 @@
D6C687F7272CD27700874C10 /* Assets.xcassets */, D6C687F7272CD27700874C10 /* Assets.xcassets */,
D6C687F9272CD27700874C10 /* LaunchScreen.storyboard */, D6C687F9272CD27700874C10 /* LaunchScreen.storyboard */,
D6C687FC272CD27700874C10 /* Info.plist */, D6C687FC272CD27700874C10 /* Info.plist */,
D6E24372278BE2B80005E546 /* read.css */,
); );
path = Reader; path = Reader;
sourceTree = "<group>"; sourceTree = "<group>";
@ -285,6 +293,14 @@
path = Items; path = Items;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D6E2436C278BD80B0005E546 /* Read */ = {
isa = PBXGroup;
children = (
D6E2436D278BD8160005E546 /* ReadViewController.swift */,
);
path = Read;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */ /* Begin PBXHeadersBuildPhase section */
@ -316,6 +332,7 @@
name = Reader; name = Reader;
packageProductDependencies = ( packageProductDependencies = (
D6E24366278BA2660005E546 /* SwiftSoup */, D6E24366278BA2660005E546 /* SwiftSoup */,
D6E24370278BE1250005E546 /* HTMLEntities */,
); );
productName = Reader; productName = Reader;
productReference = D6C687E8272CD27600874C10 /* Reader.app */; productReference = D6C687E8272CD27600874C10 /* Reader.app */;
@ -413,6 +430,7 @@
mainGroup = D6C687DF272CD27600874C10; mainGroup = D6C687DF272CD27600874C10;
packageReferences = ( packageReferences = (
D6E24365278BA2660005E546 /* XCRemoteSwiftPackageReference "SwiftSoup" */, D6E24365278BA2660005E546 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
D6E2436F278BE1250005E546 /* XCRemoteSwiftPackageReference "swift-html-entities" */,
); );
productRefGroup = D6C687E9272CD27600874C10 /* Products */; productRefGroup = D6C687E9272CD27600874C10 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -433,6 +451,7 @@
files = ( files = (
D6C687FB272CD27700874C10 /* LaunchScreen.storyboard in Resources */, D6C687FB272CD27700874C10 /* LaunchScreen.storyboard in Resources */,
D6C687F8272CD27700874C10 /* Assets.xcassets in Resources */, D6C687F8272CD27700874C10 /* Assets.xcassets in Resources */,
D6E24373278BE2B80005E546 /* read.css in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -480,6 +499,7 @@
D65B18BE275051A1004A9448 /* LocalData.swift in Sources */, D65B18BE275051A1004A9448 /* LocalData.swift in Sources */,
D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */, D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */,
D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */, D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */,
D6E2436E278BD8160005E546 /* ReadViewController.swift in Sources */,
D65B18C127505348004A9448 /* HomeViewController.swift in Sources */, D65B18C127505348004A9448 /* HomeViewController.swift in Sources */,
D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */, D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */,
); );
@ -925,6 +945,14 @@
minimumVersion = 2.3.0; 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 */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@ -933,6 +961,11 @@
package = D6E24365278BA2660005E546 /* XCRemoteSwiftPackageReference "SwiftSoup" */; package = D6E24365278BA2660005E546 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup; productName = SwiftSoup;
}; };
D6E24370278BE1250005E546 /* HTMLEntities */ = {
isa = XCSwiftPackageProductDependency;
package = D6E2436F278BE1250005E546 /* XCRemoteSwiftPackageReference "swift-html-entities" */;
productName = HTMLEntities;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */ /* Begin XCVersionGroup section */

View File

@ -206,7 +206,7 @@ extension HomeViewController: UICollectionViewDelegate {
guard let item = dataSource.itemIdentifier(for: indexPath) else { guard let item = dataSource.itemIdentifier(for: indexPath) else {
return return
} }
let vc = ItemsViewController(fervorController: fervorController, fetchRequest: item.fetchRequest) let vc = ItemsViewController(fetchRequest: item.fetchRequest, fervorController: fervorController)
vc.title = item.title vc.title = item.title
show(vc, sender: nil) show(vc, sender: nil)
} }

View File

@ -17,7 +17,7 @@ class ItemsViewController: UIViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var resultsController: NSFetchedResultsController<Item>! private var resultsController: NSFetchedResultsController<Item>!
init(fervorController: FervorController, fetchRequest: NSFetchRequest<Item>) { init(fetchRequest: NSFetchRequest<Item>, fervorController: FervorController) {
self.fervorController = fervorController self.fervorController = fervorController
self.fetchRequest = fetchRequest self.fetchRequest = fetchRequest
@ -35,6 +35,7 @@ class ItemsViewController: UIViewController {
configuration.backgroundColor = .appBackground configuration.backgroundColor = .appBackground
let layout = UICollectionViewCompositionalLayout.list(using: configuration) let layout = UICollectionViewCompositionalLayout.list(using: configuration)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.register(ItemCollectionViewCell.self, forCellWithReuseIdentifier: "itemCell") collectionView.register(ItemCollectionViewCell.self, forCellWithReuseIdentifier: "itemCell")
view.addSubview(collectionView) view.addSubview(collectionView)
@ -78,3 +79,13 @@ extension ItemsViewController: NSFetchedResultsControllerDelegate {
self.dataSource.apply(snapshot, animatingDifferences: false) 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)
}
}

View File

@ -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
}
}

View File

@ -12,7 +12,7 @@ extension UIColor {
static let appBackground = UIColor { traitCollection in static let appBackground = UIColor { traitCollection in
switch traitCollection.userInterfaceStyle { switch traitCollection.userInterfaceStyle {
case .dark: 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: case .unspecified, .light:
fallthrough fallthrough
@unknown default: @unknown default:

92
Reader/read.css Normal file
View File

@ -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);
}
}