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

@@ -14,6 +14,7 @@
950C584D58E80106DF350A22 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C0921C76676A14A024BA417 /* AppDelegate.swift */; };
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */; };
D59D3ED36ABCC1D8690A9088 /* gotcha_core.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE12480725C13293FAFDDA52 /* gotcha_core.swift */; };
D78A121E9ADA0B72E2CF94DC /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 8AD463F3AC817A44306C2297 /* MarkdownUI */; };
E7AC0B140F5CFC5EF226D174 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDAABE6B13ADC6D08D9438AF /* Assets.xcassets */; };
EC7F8B4703DDE37A0B10CD9B /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61515849F6AACD721FE915C /* AppContext.swift */; };
F55A89489B2758D694F3B27D /* Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75B3E4FFB9C9992517C4D69 /* Support.swift */; };
@@ -38,6 +39,7 @@
buildActionMask = 2147483647;
files = (
7DD332583B169B3CA1CA46E0 /* Highlighter in Frameworks */,
D78A121E9ADA0B72E2CF94DC /* MarkdownUI in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -103,6 +105,7 @@
name = Gotcha;
packageProductDependencies = (
EC5F999F50905E3801E8A71A /* Highlighter */,
8AD463F3AC817A44306C2297 /* MarkdownUI */,
);
productName = Gotcha;
productReference = 5FB3250A93766966A60A685E /* Gotcha.app */;
@@ -134,6 +137,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
85EFB86C5B2FDE8DEAD98A24 /* XCRemoteSwiftPackageReference "HighlighterSwift" */,
0B96240AE4FB1E82EE0D6B17 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = F059299C038F3CAFCE470831 /* Products */;
@@ -389,6 +393,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
0B96240AE4FB1E82EE0D6B17 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.1.0;
};
};
85EFB86C5B2FDE8DEAD98A24 /* XCRemoteSwiftPackageReference "HighlighterSwift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/smittytone/HighlighterSwift";
@@ -400,6 +412,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
8AD463F3AC817A44306C2297 /* MarkdownUI */ = {
isa = XCSwiftPackageProductDependency;
package = 0B96240AE4FB1E82EE0D6B17 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
productName = MarkdownUI;
};
EC5F999F50905E3801E8A71A /* Highlighter */ = {
isa = XCSwiftPackageProductDependency;
package = 85EFB86C5B2FDE8DEAD98A24 /* XCRemoteSwiftPackageReference "HighlighterSwift" */;

View File

@@ -1,5 +1,5 @@
{
"originHash" : "00a1c96095892a05bfd089593813422ede18735e452666e3e7a642ebd9048a45",
"originHash" : "7a8d1ff34841e518cc21434c2148b0c9b3d7c0d8b436c01ab8711755963dcc94",
"pins" : [
{
"identity" : "highlighterswift",
@@ -9,6 +9,33 @@
"revision" : "fe7aae9c9b31d3b296fd3d2dd575e1a207bb29e0",
"version" : "3.1.0"
}
},
{
"identity" : "networkimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/NetworkImage",
"state" : {
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
"version" : "6.0.1"
}
},
{
"identity" : "swift-cmark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-cmark",
"state" : {
"revision" : "924936d0427cb25a61169739a7660230bffa6ea6",
"version" : "0.8.0"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
"version" : "2.4.1"
}
}
],
"version" : 3

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)

View File

@@ -7,6 +7,9 @@ packages:
Highlighter:
url: https://github.com/smittytone/HighlighterSwift
from: 3.1.0
MarkdownUI:
url: https://github.com/gonzalezreal/swift-markdown-ui
from: 2.1.0
targets:
Gotcha:
type: application
@@ -35,6 +38,7 @@ targets:
- Generated/gotcha_core.swift
dependencies:
- package: Highlighter
- package: MarkdownUI
preBuildScripts:
- name: Build Rust core
basedOnDependencyAnalysis: false