From 50bb8f94c281eeb4d41b6b0087baae5f70cfc725 Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Fri, 31 Jul 2026 12:02:41 +0200 Subject: [PATCH] Add Markdown file previews --- ios/Gotcha.xcodeproj/project.pbxproj | 17 +++++ .../xcshareddata/swiftpm/Package.resolved | 29 ++++++- ios/Sources/DetailScreens.swift | 75 ++++++++++++++++++- ios/project.yml | 4 + 4 files changed, 123 insertions(+), 2 deletions(-) diff --git a/ios/Gotcha.xcodeproj/project.pbxproj b/ios/Gotcha.xcodeproj/project.pbxproj index efd0f7d..0130e54 100644 --- a/ios/Gotcha.xcodeproj/project.pbxproj +++ b/ios/Gotcha.xcodeproj/project.pbxproj @@ -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" */; diff --git a/ios/Gotcha.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Gotcha.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 97d9c79..5f9e196 100644 --- a/ios/Gotcha.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Gotcha.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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 diff --git a/ios/Sources/DetailScreens.swift b/ios/Sources/DetailScreens.swift index 06f8e7a..fb617c6 100644 --- a/ios/Sources/DetailScreens.swift +++ b/ios/Sources/DetailScreens.swift @@ -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? 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) diff --git a/ios/project.yml b/ios/project.yml index f0b936d..3056f64 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -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