diff --git a/Reader.xcodeproj/project.pbxproj b/Reader.xcodeproj/project.pbxproj index 72b2de5..b3fdc23 100644 --- a/Reader.xcodeproj/project.pbxproj +++ b/Reader.xcodeproj/project.pbxproj @@ -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 = ""; }; D6E24368278BABB40005E546 /* UIColor+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+App.swift"; sourceTree = ""; }; D6E2436A278BB1880005E546 /* HomeCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCollectionViewCell.swift; sourceTree = ""; }; + D6E2436D278BD8160005E546 /* ReadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadViewController.swift; sourceTree = ""; }; + D6E24372278BE2B80005E546 /* read.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = read.css; sourceTree = ""; }; /* 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 = ""; @@ -237,6 +244,7 @@ D6C687F7272CD27700874C10 /* Assets.xcassets */, D6C687F9272CD27700874C10 /* LaunchScreen.storyboard */, D6C687FC272CD27700874C10 /* Info.plist */, + D6E24372278BE2B80005E546 /* read.css */, ); path = Reader; sourceTree = ""; @@ -285,6 +293,14 @@ path = Items; sourceTree = ""; }; + D6E2436C278BD80B0005E546 /* Read */ = { + isa = PBXGroup; + children = ( + D6E2436D278BD8160005E546 /* ReadViewController.swift */, + ); + path = Read; + sourceTree = ""; + }; /* 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 */ diff --git a/Reader/Screens/Home/HomeViewController.swift b/Reader/Screens/Home/HomeViewController.swift index f06ad24..50f2973 100644 --- a/Reader/Screens/Home/HomeViewController.swift +++ b/Reader/Screens/Home/HomeViewController.swift @@ -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) } diff --git a/Reader/Screens/Items/ItemsViewController.swift b/Reader/Screens/Items/ItemsViewController.swift index 369c64b..ab1bfef 100644 --- a/Reader/Screens/Items/ItemsViewController.swift +++ b/Reader/Screens/Items/ItemsViewController.swift @@ -17,7 +17,7 @@ class ItemsViewController: UIViewController { private var dataSource: UICollectionViewDiffableDataSource! private var resultsController: NSFetchedResultsController! - init(fervorController: FervorController, fetchRequest: NSFetchRequest) { + init(fetchRequest: NSFetchRequest, 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) + } +} diff --git a/Reader/Screens/Read/ReadViewController.swift b/Reader/Screens/Read/ReadViewController.swift new file mode 100644 index 0000000..d20ccbd --- /dev/null +++ b/Reader/Screens/Read/ReadViewController.swift @@ -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 += "

" + if let url = item.url { + info += "" + } + info += title.htmlEscape() + if item.url != nil { + info += "" + } + info += "

" + } + if let feedTitle = item.feed!.title, !feedTitle.isEmpty { + info += "

\(feedTitle.htmlEscape())

" + } + if let author = item.author, !author.isEmpty { + info += "

\(author)

" + } + if let published = item.published { + let formatted = ReadViewController.publishedFormatter.string(from: published) + info += "

\(formatted)

" + } + return """ + + + + + + + + +
+ \(info) +
+
+ \(content) +
+ + + """ + } + +} + +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 + } +} diff --git a/Reader/UIColor+App.swift b/Reader/UIColor+App.swift index af82c61..b33e8be 100644 --- a/Reader/UIColor+App.swift +++ b/Reader/UIColor+App.swift @@ -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: diff --git a/Reader/read.css b/Reader/read.css new file mode 100644 index 0000000..def5c4f --- /dev/null +++ b/Reader/read.css @@ -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); + } +}