mirror of
https://github.com/zhigang1992/GitHawk.git
synced 2026-04-24 04:05:16 +08:00
Add PDF support (#1596)
* Add string extension for searching pdf files (#1552) * Create RepositoryWebViewController (#1552) * Initial presenting of the pdf files (#1552) * refactor: Rename String+Resource -> String+BinaryFile (#1552) * Build destination URL (#1552) * Configure title (#1552) * Encode file path * Use EmptyView for errors * Replace UIWebView with WKWebView * Review fixes * refactor: Make binarySuffix computed property * Unify if-else path * Group guard statements
This commit is contained in:
@@ -46,3 +46,35 @@ struct FilePath {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - FilePath (BinaryFile) -
|
||||
|
||||
extension FilePath {
|
||||
|
||||
private static let supportedBinaries = [
|
||||
"pdf": "application/pdf"
|
||||
]
|
||||
|
||||
// MARK: Public API
|
||||
|
||||
/// A Boolean value indicating whether a string has binary file suffix.
|
||||
///
|
||||
/// Supported types: **pdf**.
|
||||
var hasBinarySuffix: Bool {
|
||||
return binarySuffix != nil
|
||||
}
|
||||
|
||||
/// Returns mime type for the supported binary files.
|
||||
var mimeType: String? {
|
||||
guard let type = binarySuffix else { return nil }
|
||||
|
||||
return FilePath.supportedBinaries[type]
|
||||
}
|
||||
|
||||
// MARK: Private API
|
||||
|
||||
private var binarySuffix: String? {
|
||||
return FilePath.supportedBinaries.keys.first(where: { path.hasSuffix($0) })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -130,23 +130,50 @@ ListSingleSectionControllerDelegate {
|
||||
|
||||
func didSelect(_ sectionController: ListSingleSectionController, with object: Any) {
|
||||
guard let file = object as? RepositoryFile else { return }
|
||||
let next = path.appending(file.name)
|
||||
let controller: UIViewController
|
||||
let nextPath = path.appending(file.name)
|
||||
|
||||
if file.isDirectory {
|
||||
controller = RepositoryCodeDirectoryViewController(
|
||||
client: client,
|
||||
showDirectory(at: nextPath)
|
||||
} else {
|
||||
showFile(at: nextPath)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - RepositoryCodeDirectoryViewController (Navigation) -
|
||||
|
||||
extension RepositoryCodeDirectoryViewController {
|
||||
|
||||
private func showDirectory(at path: FilePath) {
|
||||
let controller = RepositoryCodeDirectoryViewController(
|
||||
client: client,
|
||||
repo: repo,
|
||||
branch: branch,
|
||||
path: path
|
||||
)
|
||||
|
||||
navigationController?.pushViewController(controller, animated: trueUnlessReduceMotionEnabled)
|
||||
}
|
||||
|
||||
private func showFile(at path: FilePath) {
|
||||
let controller: UIViewController
|
||||
|
||||
if path.hasBinarySuffix {
|
||||
controller = RepositoryWebViewController(
|
||||
repo: repo,
|
||||
branch: branch,
|
||||
path: next
|
||||
path: path
|
||||
)
|
||||
} else {
|
||||
controller = RepositoryCodeBlobViewController(
|
||||
client: client,
|
||||
repo: repo,
|
||||
branch: branch,
|
||||
path: next
|
||||
path: path
|
||||
)
|
||||
}
|
||||
|
||||
navigationController?.pushViewController(controller, animated: trueUnlessReduceMotionEnabled)
|
||||
}
|
||||
|
||||
|
||||
203
Classes/Repository/RepositoryWebViewController.swift
Normal file
203
Classes/Repository/RepositoryWebViewController.swift
Normal file
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// RepositoryWebViewController.swift
|
||||
// Freetime
|
||||
//
|
||||
// Created by Ivan Magda on 01/03/2018.
|
||||
// Copyright © 2018 Ryan Nystrom. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
// MARK: RepositoryWebViewController: UIViewController
|
||||
|
||||
final class RepositoryWebViewController: UIViewController {
|
||||
|
||||
// MARK: Types
|
||||
|
||||
private enum State {
|
||||
case idle
|
||||
case fetching
|
||||
case error(String?)
|
||||
}
|
||||
|
||||
// MARK: Instance Variables
|
||||
|
||||
private let branch: String
|
||||
private let path: FilePath
|
||||
private let repo: RepositoryDetails
|
||||
|
||||
private var state = State.idle {
|
||||
didSet {
|
||||
switch state {
|
||||
case .idle:
|
||||
activityIndicator.stopAnimating()
|
||||
emptyView.isHidden = true
|
||||
webView.isHidden = false
|
||||
case .fetching:
|
||||
activityIndicator.startAnimating()
|
||||
emptyView.isHidden = true
|
||||
webView.isHidden = false
|
||||
case .error(let message):
|
||||
activityIndicator.stopAnimating()
|
||||
webView.isHidden = true
|
||||
emptyView.isHidden = false
|
||||
emptyView.label.text = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var resourceURL: URL? {
|
||||
guard let encodedPath = path.path.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return URL(string: "https://github.com/\(repo.owner)/\(repo.name)/raw/\(branch)/\(encodedPath)")
|
||||
}
|
||||
|
||||
private lazy var webView: WKWebView = {
|
||||
let webView = WKWebView()
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
webView.navigationDelegate = self
|
||||
webView.backgroundColor = .white
|
||||
|
||||
return webView
|
||||
}()
|
||||
|
||||
private let emptyView = EmptyView()
|
||||
|
||||
private lazy var activityIndicator: UIActivityIndicatorView = {
|
||||
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
|
||||
activityIndicator.hidesWhenStopped = true
|
||||
|
||||
return activityIndicator
|
||||
}()
|
||||
|
||||
// MARK: Init
|
||||
|
||||
init(
|
||||
repo: RepositoryDetails,
|
||||
branch: String,
|
||||
path: FilePath
|
||||
) {
|
||||
self.repo = repo
|
||||
self.branch = branch
|
||||
self.path = path
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: UIViewController Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setup()
|
||||
fetch()
|
||||
}
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
super.viewWillLayoutSubviews()
|
||||
|
||||
emptyView.frame = view.bounds
|
||||
activityIndicator.center = view.center
|
||||
|
||||
if let tabBarController = tabBarController {
|
||||
webView.scrollView.contentInset.bottom = tabBarController.tabBar.bounds.height
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - RepositoryWebViewController (UIWebViewDelegate) -
|
||||
|
||||
extension RepositoryWebViewController: WKNavigationDelegate {
|
||||
|
||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||
state = .fetching
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
state = .idle
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
showError(cannotLoad: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - RepositoryWebViewController (Fetch Data) -
|
||||
|
||||
extension RepositoryWebViewController {
|
||||
|
||||
private func fetch() {
|
||||
guard let url = resourceURL else { return showError(cannotLoad: false) }
|
||||
state = .fetching
|
||||
|
||||
URLSession.shared.dataTask(with: url) { [weak self] (data, _, error) in
|
||||
DispatchQueue.main.async {
|
||||
guard let strongSelf = self else { return }
|
||||
guard error == nil,
|
||||
let data = data else { return strongSelf.showError(cannotLoad: true) }
|
||||
|
||||
strongSelf.webView.load(
|
||||
data,
|
||||
mimeType: strongSelf.path.mimeType ?? "text/plain",
|
||||
characterEncodingName: "UTF-8",
|
||||
baseURL: url
|
||||
)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - RepositoryWebViewController (Actions) -
|
||||
|
||||
extension RepositoryWebViewController {
|
||||
|
||||
@objc private func onFileNavigationTitle(sender: UIView) {
|
||||
showAlert(filePath: path, sender: sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - RepositoryWebViewController (Private Helpers) -
|
||||
|
||||
extension RepositoryWebViewController {
|
||||
|
||||
private func setup() {
|
||||
makeBackBarItemEmpty()
|
||||
configureTitle(
|
||||
filePath: path,
|
||||
target: self,
|
||||
action: #selector(onFileNavigationTitle(sender:))
|
||||
)
|
||||
|
||||
state = .idle
|
||||
emptyView.isHidden = true
|
||||
|
||||
view.backgroundColor = .white
|
||||
view.addSubview(emptyView)
|
||||
view.addSubview(webView)
|
||||
view.addSubview(activityIndicator)
|
||||
|
||||
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)
|
||||
])
|
||||
}
|
||||
|
||||
private func showError(cannotLoad: Bool) {
|
||||
state = .error(cannotLoad
|
||||
? NSLocalizedString("Cannot display file", comment: "")
|
||||
: NSLocalizedString("Error loading file", comment: "")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -359,6 +359,7 @@
|
||||
5DB4DD471FC5C10000DF7ABF /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DB4DD461FC5C10000DF7ABF /* Accessibility.swift */; };
|
||||
65A315292044369D0074E3B6 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A315282044369D0074E3B6 /* TabBarController.swift */; };
|
||||
65A3152B2044376D0074E3B6 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A3152A2044376D0074E3B6 /* Route.swift */; };
|
||||
7509865A2048959D00D1E37A /* RepositoryWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750986592048959D00D1E37A /* RepositoryWebViewController.swift */; };
|
||||
754488B11F7ADF8D0032D08C /* UIAlertController+Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754488B01F7ADF8D0032D08C /* UIAlertController+Action.swift */; };
|
||||
75468F7A1F7AFBC800F2BC19 /* AlertActionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75468F791F7AFBC800F2BC19 /* AlertActionBuilder.swift */; };
|
||||
75A0ACF51F79A82D0062D99A /* AlertAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A0ACF41F79A82D0062D99A /* AlertAction.swift */; };
|
||||
@@ -795,6 +796,7 @@
|
||||
63A98647141C04CBCFCA7923 /* Pods-Freetime.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Freetime.release.xcconfig"; path = "Pods/Target Support Files/Pods-Freetime/Pods-Freetime.release.xcconfig"; sourceTree = "<group>"; };
|
||||
65A315282044369D0074E3B6 /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = "<group>"; };
|
||||
65A3152A2044376D0074E3B6 /* Route.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = "<group>"; };
|
||||
750986592048959D00D1E37A /* RepositoryWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryWebViewController.swift; sourceTree = "<group>"; };
|
||||
754488B01F7ADF8D0032D08C /* UIAlertController+Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Action.swift"; sourceTree = "<group>"; };
|
||||
75468F791F7AFBC800F2BC19 /* AlertActionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertActionBuilder.swift; sourceTree = "<group>"; };
|
||||
75A0ACF41F79A82D0062D99A /* AlertAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertAction.swift; sourceTree = "<group>"; };
|
||||
@@ -1759,6 +1761,7 @@
|
||||
986B873D1F2E1CE400AAB55C /* RepositoryClient.swift */,
|
||||
29B0EF861F93DF6C00870291 /* RepositoryCodeBlobViewController.swift */,
|
||||
29AF1E8B1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift */,
|
||||
750986592048959D00D1E37A /* RepositoryWebViewController.swift */,
|
||||
295A77BD1F75C1CC007BC403 /* RepositoryDetails.swift */,
|
||||
29AF1E8D1F8ABC900008A0EF /* RepositoryFile.swift */,
|
||||
29B94E6E1FCB743900715D7E /* RepositoryFileCell.swift */,
|
||||
@@ -2333,6 +2336,7 @@
|
||||
297403DD1F185A8700ABA95A /* IssueAssigneeUserCell.swift in Sources */,
|
||||
2999972620310E9700995FFD /* IssueMergeModel.swift in Sources */,
|
||||
2999972820310F3100995FFD /* IssueMergeContextModel.swift in Sources */,
|
||||
7509865A2048959D00D1E37A /* RepositoryWebViewController.swift in Sources */,
|
||||
29CC294B1FF83488006B6DE7 /* IssueCommentModel+Inset.swift in Sources */,
|
||||
297403D31F1850DC00ABA95A /* IssueAssigneeViewModel.swift in Sources */,
|
||||
292FCAFD1EDFCC510026635E /* IssueCollapsedBodies.swift in Sources */,
|
||||
|
||||
Reference in New Issue
Block a user