|
| 1 | +// |
| 2 | +// ZLRepoCodePreview4Controller.swift |
| 3 | +// ZLGitHubClient |
| 4 | +// |
| 5 | +// Created by 朱猛 on 2020/7/14. |
| 6 | +// Copyright © 2020 ZM. All rights reserved. |
| 7 | +// |
| 8 | + |
| 9 | +import UIKit |
| 10 | +import WebKit |
| 11 | +import ZLUIUtilities |
| 12 | +import ZLBaseExtension |
| 13 | +import ZLGitRemoteService |
| 14 | +import ZLUtilities |
| 15 | +import ZMMVVM |
| 16 | + |
| 17 | +/** |
| 18 | + * 利用 REST API 获取 md 内容 ; 代码使用markdown接口渲染 |
| 19 | + */ |
| 20 | + |
| 21 | +class ZLRepoCodePreview4Controller: ZMViewController { |
| 22 | + |
| 23 | + // model |
| 24 | + let contentModel: ZLGithubContentModel |
| 25 | + let repoFullName: String |
| 26 | + let branch: String |
| 27 | + |
| 28 | + var htmlStr: String? |
| 29 | + var rawContent: String? |
| 30 | + |
| 31 | + var theme: String = "vs" |
| 32 | + |
| 33 | + |
| 34 | + init(repoFullName: String, contentModel: ZLGithubContentModel, branch: String) { |
| 35 | + self.repoFullName = repoFullName |
| 36 | + self.contentModel = contentModel |
| 37 | + self.branch = branch |
| 38 | + super.init(nibName: nil, bundle: nil) |
| 39 | + } |
| 40 | + |
| 41 | + required init?(coder: NSCoder) { |
| 42 | + fatalError("init(coder:) has not been implemented") |
| 43 | + } |
| 44 | + |
| 45 | + deinit { |
| 46 | + NotificationCenter.default.removeObserver(self, name: ZLUserInterfaceStyleChange_Notification, object: nil) |
| 47 | + } |
| 48 | + |
| 49 | + override func viewDidLoad() { |
| 50 | + super.viewDidLoad() |
| 51 | + requestRawFileContent() |
| 52 | + } |
| 53 | + |
| 54 | + override func viewDidAppear(_ animated: Bool) { |
| 55 | + super.viewDidAppear(animated) |
| 56 | + |
| 57 | + if ZLDeviceInfo.isIPhone() { |
| 58 | + guard let appdelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate else { |
| 59 | + return |
| 60 | + } |
| 61 | + appdelegate.allowRotation = true |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + override func viewWillDisappear(_ animated: Bool) { |
| 66 | + super.viewWillDisappear(animated) |
| 67 | + |
| 68 | + if ZLDeviceInfo.isIPhone() { |
| 69 | + guard let appdelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate else { |
| 70 | + return |
| 71 | + } |
| 72 | + appdelegate.allowRotation = false |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { |
| 77 | + |
| 78 | + super.viewWillTransition(to: size, with: coordinator) |
| 79 | + |
| 80 | + if ZLDeviceInfo.isIPhone() { |
| 81 | + guard let navigationVC: ZMNavigationController = self.navigationController as? ZMNavigationController else { |
| 82 | + return |
| 83 | + } |
| 84 | + if size.height > size.width { |
| 85 | + // 横屏变竖屏 |
| 86 | + self.isZmNavigationBarHidden = false |
| 87 | + navigationVC.forbidGestureBack = false |
| 88 | + } else { |
| 89 | + self.isZmNavigationBarHidden = true |
| 90 | + navigationVC.forbidGestureBack = true |
| 91 | + } |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + override func setupUI() { |
| 96 | + super.setupUI() |
| 97 | + self.title = self.contentModel.path |
| 98 | + |
| 99 | + self.zmNavigationBar.backButton.isHidden = false |
| 100 | + self.zmNavigationBar.addRightView(moreButton) |
| 101 | + |
| 102 | + self.contentView.addSubview(webView) |
| 103 | + webView.snp.makeConstraints({(make) in |
| 104 | + make.edges.equalToSuperview() |
| 105 | + }) |
| 106 | + |
| 107 | + self.contentView.addSubview(switchButton) |
| 108 | + |
| 109 | + switchButton.snp.makeConstraints { make in |
| 110 | + make.left.equalTo(20) |
| 111 | + make.top.equalTo(20) |
| 112 | + make.width.equalTo(60) |
| 113 | + make.height.equalTo(30) |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + // view |
| 118 | + lazy var webView: WKWebView = { |
| 119 | + let wv = WKWebView(frame: CGRect.init()) |
| 120 | + wv.backgroundColor = UIColor.clear |
| 121 | + wv.scrollView.backgroundColor = UIColor.clear |
| 122 | + wv.scrollView.bounces = false |
| 123 | + wv.uiDelegate = self |
| 124 | + wv.navigationDelegate = self |
| 125 | + return wv |
| 126 | + }() |
| 127 | + |
| 128 | + lazy var moreButton: UIButton = { |
| 129 | + let button = UIButton.init(type: .custom) |
| 130 | + button.setAttributedTitle(NSAttributedString(string: ZLIconFont.More.rawValue, |
| 131 | + attributes: [.font: UIFont.zlIconFont(withSize: 30), |
| 132 | + .foregroundColor: UIColor.label(withName: "ICON_Common")]), |
| 133 | + for: .normal) |
| 134 | + button.snp.makeConstraints({ make in |
| 135 | + make.size.equalTo(60) |
| 136 | + }) |
| 137 | + button.addTarget(self, action: #selector(onMoreButtonClick(button:)), for: .touchUpInside) |
| 138 | + return button |
| 139 | + }() |
| 140 | + |
| 141 | + lazy var switchButton: UIButton = { |
| 142 | + let button = ZMButton() |
| 143 | + button.setTitle("切换主题", for: .normal) |
| 144 | + button.addTarget(self, action: #selector(onSwitchButtonClick(button:)), for: .touchUpInside) |
| 145 | + return button |
| 146 | + }() |
| 147 | +} |
| 148 | + |
| 149 | +extension ZLRepoCodePreview4Controller { |
| 150 | + |
| 151 | + static func getThemeSelectView() -> ZMSingleSelectTitlePopView { |
| 152 | + /// 开发语言选择 |
| 153 | + let dateRangeSelectView = ZMSingleSelectTitlePopView() |
| 154 | + dateRangeSelectView.frame = UIScreen.main.bounds |
| 155 | + let title = "主题" |
| 156 | + dateRangeSelectView.titleLabel.text = title |
| 157 | + let placeHolder = "筛选主题" |
| 158 | + .asMutableAttributedString() |
| 159 | + .font(.zlRegularFont(withSize: 12)) |
| 160 | + .foregroundColor(ZLRGBValue_H(colorValue: 0xCED1D6)) |
| 161 | + dateRangeSelectView.textField.attributedPlaceholder = placeHolder |
| 162 | + dateRangeSelectView.contentWidth = 280 |
| 163 | + dateRangeSelectView.contentHeight = 500 |
| 164 | + dateRangeSelectView.collectionView.lineSpacing = .leastNonzeroMagnitude |
| 165 | + dateRangeSelectView.collectionView.interitemSpacing = .leastNonzeroMagnitude |
| 166 | + dateRangeSelectView.collectionView.itemSize = CGSize(width: 280, height: 50) |
| 167 | + dateRangeSelectView.popDelegate = ZMLanguageSelectView.popDelegate |
| 168 | + return dateRangeSelectView |
| 169 | + } |
| 170 | + |
| 171 | + /// 弹出开发语言选择框 |
| 172 | + static func showThemeSelectView(to: UIView, |
| 173 | + theme: String?, |
| 174 | + resultBlock : @escaping ((String?) -> Void)) { |
| 175 | + |
| 176 | + |
| 177 | + let themes = [ |
| 178 | + "1c-light", |
| 179 | + "a11y-dark", |
| 180 | + "a11y-light", |
| 181 | + "agate", |
| 182 | + "an-old-hope", |
| 183 | + "androidstudio", |
| 184 | + "arduino-light", |
| 185 | + "arta", |
| 186 | + "ascetic", |
| 187 | + "atom-one-dark-reasonable", |
| 188 | + "atom-one-dark", |
| 189 | + "atom-one-light", |
| 190 | + "brown-paper", |
| 191 | + "codepen-embed", |
| 192 | + "color-brewer", |
| 193 | + "cybertopia-cherry", |
| 194 | + "cybertopia-dimmer", |
| 195 | + "cybertopia-icecap", |
| 196 | + "cybertopia-saturated", |
| 197 | + "dark", |
| 198 | + "default", |
| 199 | + "devibeans", |
| 200 | + "docco", |
| 201 | + "far", |
| 202 | + "felipec", |
| 203 | + "foundation", |
| 204 | + "github-dark-dimmed", |
| 205 | + "github-dark", |
| 206 | + "github", |
| 207 | + "gml", |
| 208 | + "googlecode", |
| 209 | + "gradient-dark", |
| 210 | + "gradient-light", |
| 211 | + "grayscale", |
| 212 | + "hybrid", |
| 213 | + "idea", |
| 214 | + "intellij-light", |
| 215 | + "ir-black", |
| 216 | + "isbl-editor-dark", |
| 217 | + "isbl-editor-light", |
| 218 | + "kimbie-dark", |
| 219 | + "kimbie-light", |
| 220 | + "lightfair", |
| 221 | + "lioshi", |
| 222 | + "magula", |
| 223 | + "mono-blue", |
| 224 | + "monokai-sublime", |
| 225 | + "monokai", |
| 226 | + "night-owl", |
| 227 | + "nnfx-dark", |
| 228 | + "nnfx-light", |
| 229 | + "nord", |
| 230 | + "obsidian", |
| 231 | + "panda-syntax-dark", |
| 232 | + "panda-syntax-light", |
| 233 | + "paraiso-dark", |
| 234 | + "paraiso-light", |
| 235 | + "pojoaque", |
| 236 | + "purebasic", |
| 237 | + "qtcreator-dark", |
| 238 | + "qtcreator-light", |
| 239 | + "rainbow", |
| 240 | + "rose-pine-dawn", |
| 241 | + "rose-pine-moon", |
| 242 | + "rose-pine", |
| 243 | + "routeros", |
| 244 | + "school-book", |
| 245 | + "shades-of-purple", |
| 246 | + "srcery", |
| 247 | + "stackoverflow-dark", |
| 248 | + "stackoverflow-light", |
| 249 | + "sunburst", |
| 250 | + "tokyo-night-dark", |
| 251 | + "tokyo-night-light", |
| 252 | + "tomorrow-night-blue", |
| 253 | + "tomorrow-night-bright", |
| 254 | + "vs", |
| 255 | + "vs2015", |
| 256 | + "xcode", |
| 257 | + "xt256" |
| 258 | + ] |
| 259 | + var selectedIndex = themes.firstIndex(of: theme ?? "") ?? 0 |
| 260 | + |
| 261 | + |
| 262 | + Self.getThemeSelectView() |
| 263 | + .showSingleSelectTitleBox(to, |
| 264 | + contentPoition: .center, |
| 265 | + animationDuration: 0.1, |
| 266 | + titles: themes, |
| 267 | + selectedIndex: selectedIndex, |
| 268 | + cellType: ZMDevelopmentLanguageSelectTickCell.self) |
| 269 | + { index, title in |
| 270 | + resultBlock(title) |
| 271 | + } |
| 272 | + |
| 273 | + } |
| 274 | + |
| 275 | +} |
| 276 | + |
| 277 | +// MARK: - Action |
| 278 | +extension ZLRepoCodePreview4Controller { |
| 279 | + |
| 280 | + @objc func onMoreButtonClick(button: UIButton) { |
| 281 | + |
| 282 | + guard let url = URL(string: self.contentModel.html_url) else { |
| 283 | + return |
| 284 | + } |
| 285 | + button.showShareMenu(title: url.absoluteString, url: url, sourceViewController: self ) |
| 286 | + } |
| 287 | + |
| 288 | + @objc func onSwitchButtonClick(button: UIButton) { |
| 289 | + Self.showThemeSelectView(to: self.view, theme: self.theme) { [weak self] theme in |
| 290 | + guard let self else { return } |
| 291 | + self.theme = theme ?? "" |
| 292 | + self.generateHTML(rawContent: self.rawContent ?? "") |
| 293 | + self.startLoadCode() |
| 294 | + |
| 295 | + } |
| 296 | + } |
| 297 | +} |
| 298 | + |
| 299 | +// MARK: - Request |
| 300 | +extension ZLRepoCodePreview4Controller { |
| 301 | + |
| 302 | + func generateHTML(rawContent: String) { |
| 303 | + let htmlURL: URL? = Bundle.main.url(forResource: "highlight", withExtension: "html") |
| 304 | + |
| 305 | + if let url = htmlURL { |
| 306 | + |
| 307 | + do { |
| 308 | + let htmlStr = try String.init(contentsOf: url) |
| 309 | + let newHtmlStr = NSMutableString.init(string: htmlStr) |
| 310 | + |
| 311 | + let headRange = (newHtmlStr as NSString).range(of: "</head>") |
| 312 | + if headRange.location != NSNotFound { |
| 313 | + newHtmlStr.insert("<link rel=\"stylesheet\" href=\"./\(self.theme).min.css\">", at: headRange.location) |
| 314 | + } |
| 315 | + |
| 316 | + let codeRange = (newHtmlStr as NSString).range(of: "</code>") |
| 317 | + if codeRange.location != NSNotFound { |
| 318 | + newHtmlStr.insert(rawContent.htmlEscaped(), at: codeRange.location) |
| 319 | + } |
| 320 | + |
| 321 | + self.htmlStr = newHtmlStr as String |
| 322 | + |
| 323 | + } catch { |
| 324 | + ZLToastView.showMessage("load Code index html failed") |
| 325 | + } |
| 326 | + } |
| 327 | + } |
| 328 | + |
| 329 | + func startLoadCode() { |
| 330 | + webView.loadHTMLString(self.htmlStr ?? "", baseURL: Bundle.main.bundleURL) |
| 331 | + } |
| 332 | +} |
| 333 | + |
| 334 | +// MARK: - Request |
| 335 | +extension ZLRepoCodePreview4Controller { |
| 336 | + func requestRawFileContent() { |
| 337 | + |
| 338 | + self.view.showProgressHUD() |
| 339 | + ZLRepoServiceShared()?.getRepositoryFileRawInfo(withFullName: self.repoFullName, |
| 340 | + path: self.contentModel.path, |
| 341 | + branch: self.branch, |
| 342 | + serialNumber: NSString.generateSerialNumber()) |
| 343 | + {[weak self] (resultModel: ZLOperationResultModel) in |
| 344 | + |
| 345 | + guard let self = self else { return } |
| 346 | + self.view.dismissProgressHUD() |
| 347 | + |
| 348 | + if resultModel.result, |
| 349 | + let data: String = resultModel.data as? String { |
| 350 | + self.rawContent = data |
| 351 | + self.generateHTML(rawContent: data) |
| 352 | + self.startLoadCode() |
| 353 | + } else { |
| 354 | + if let errorModel = resultModel.data as? ZLGithubRequestErrorModel { |
| 355 | + ZLToastView.showMessage(errorModel.message) |
| 356 | + } |
| 357 | + } |
| 358 | + } |
| 359 | + } |
| 360 | +} |
| 361 | + |
| 362 | +extension ZLRepoCodePreview4Controller: WKUIDelegate, WKNavigationDelegate { |
| 363 | + |
| 364 | +} |
| 365 | + |
| 366 | + |
| 367 | +extension String { |
| 368 | + func htmlEscaped() -> String { |
| 369 | + return self |
| 370 | + .replacingOccurrences(of: "&", with: "&") |
| 371 | + .replacingOccurrences(of: "<", with: "<") |
| 372 | + .replacingOccurrences(of: ">", with: ">") |
| 373 | + .replacingOccurrences(of: "\"", with: """) |
| 374 | + .replacingOccurrences(of: "'", with: "'") |
| 375 | + } |
| 376 | +} |
0 commit comments