Add Markdown file previews

This commit is contained in:
Georg Bauer
2026-07-31 12:02:41 +02:00
parent 178094fd0f
commit 50bb8f94c2
4 changed files with 123 additions and 2 deletions

View File

@@ -1,5 +1,7 @@
import Highlighter
import MarkdownUI
import QuickLook
import SwiftUI
import UIKit
import UniformTypeIdentifiers
@@ -336,6 +338,8 @@ final class RepositoryDirectoryViewController: RefreshingTableViewController {
@MainActor
final class RepositoryFileViewController: UIViewController, QLPreviewControllerDataSource {
private enum MarkdownMode: Int { case preview, source }
private let context: AppContext
private let owner: String
private let repository: String
@@ -343,6 +347,9 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
private let spinner = UIActivityIndicatorView(style: .medium)
private var loadingTask: Task<Void, Never>?
private var previewURL: URL?
private var markdownSource: String?
private var displayedController: UIViewController?
private var displayedView: UIView?
private weak var sourceScrollView: UIScrollView?
private weak var sourceTextView: UITextView?
@@ -370,7 +377,11 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
path: path
)
if !isMediaFile(path), let source = String(data: data, encoding: .utf8) {
showSource(source)
if isMarkdownFile {
showMarkdown(source)
} else {
showSource(source)
}
} else {
try showQuickLook(data)
}
@@ -396,6 +407,10 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
|| type.conforms(to: .pdf)
}
private var isMarkdownFile: Bool {
["md", "markdown", "mdown", "mkd"].contains((path as NSString).pathExtension.lowercased())
}
deinit {
loadingTask?.cancel()
if let previewURL { try? FileManager.default.removeItem(at: previewURL) }
@@ -409,7 +424,40 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
previewURL! as NSURL
}
private func showMarkdown(_ source: String) {
markdownSource = source
navigationItem.prompt = title
let modeControl = UISegmentedControl(items: ["Preview", "Source"])
modeControl.selectedSegmentIndex = MarkdownMode.preview.rawValue
modeControl.addTarget(self, action: #selector(markdownModeChanged(_:)), for: .valueChanged)
modeControl.accessibilityLabel = "Markdown view"
navigationItem.titleView = modeControl
showMarkdownPreview(source)
}
@objc private func markdownModeChanged(_ sender: UISegmentedControl) {
guard let mode = MarkdownMode(rawValue: sender.selectedSegmentIndex),
let source = markdownSource else { return }
switch mode {
case .preview: showMarkdownPreview(source)
case .source: showSource(source)
}
}
private func showMarkdownPreview(_ source: String) {
removeDisplayedView()
let preview = UIHostingController(rootView: RepositoryMarkdownPreview(source: source))
addChild(preview)
preview.view.frame = view.bounds
preview.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
displayedController = preview
displayedView = preview.view
view.addSubview(preview.view)
preview.didMove(toParent: self)
}
private func showSource(_ source: String) {
removeDisplayedView()
let highlighter = Highlighter()
highlighter?.setTheme(
traitCollection.userInterfaceStyle == .dark ? "atom-one-dark" : "atom-one-light"
@@ -445,10 +493,21 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
sourceScrollView = scrollView
sourceTextView = textView
scrollView.addSubview(textView)
displayedView = scrollView
view.addSubview(scrollView)
updateSourceLayout()
}
private func removeDisplayedView() {
displayedController?.willMove(toParent: nil)
displayedView?.removeFromSuperview()
displayedController?.removeFromParent()
displayedController = nil
displayedView = nil
sourceScrollView = nil
sourceTextView = nil
}
private func updateSourceLayout() {
guard let scrollView = sourceScrollView, let textView = sourceTextView else { return }
textView.textContainer.widthTracksTextView = false
@@ -488,6 +547,20 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
}
}
private struct RepositoryMarkdownPreview: View {
let source: String
var body: some View {
ScrollView {
Markdown(source)
.markdownTheme(.gitHub)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
.background(Color(uiColor: .systemBackground))
}
}
enum DiffSource {
case commit(owner: String, repository: String, sha: String, path: String)
case pull(owner: String, repository: String, number: Int64, path: String)