From 3ae6f954f0a29959944fff1463730b690425ead6 Mon Sep 17 00:00:00 2001 From: Patrick Schiller Date: Sat, 1 Aug 2026 08:26:45 +0200 Subject: [PATCH] Align iOS capture methods and SourceBraid rebrand Signed-off-by: Patrick Schiller --- ios/README.md | 11 +++--- ios/SourceBraid.xcodeproj/project.pbxproj | 20 +++++------ ios/SourceBraid/Resources/Info.plist | 2 +- .../Resources/SourceBraid.entitlements | 4 +-- ios/SourceBraid/Shared/ClipBuilder.swift | 35 +++++++++++++++---- ios/SourceBraid/Shared/GitHubClient.swift | 7 ++++ .../Shared/SourceBraidEnvironment.swift | 5 +-- ios/SourceBraidShare/Info.plist | 2 +- ios/SourceBraidShare/ShareViewModel.swift | 2 +- .../SourceBraidShare.entitlements | 4 +-- ios/SourceBraidShare/WebPageCapture.js | 32 +++++++++++++++-- ios/SourceBraidShare/WebPageCapture.swift | 1 + ios/SourceBraidTests/ClipBuilderTests.swift | 15 +++++--- 13 files changed, 103 insertions(+), 37 deletions(-) diff --git a/ios/README.md b/ios/README.md index 711e814..d0b514d 100644 --- a/ios/README.md +++ b/ios/README.md @@ -8,8 +8,8 @@ The iOS project contains a SwiftUI configuration app and a native Share Extensio 2. Select the **SourceBraid** project and the **SourceBraid** target. 3. Under **Signing & Capabilities**, select your Apple Developer team. 4. Repeat for the **SourceBraidShare** target. -5. If the bundle identifiers are unavailable, replace `de.patrickschiller.sourcebraid` and `de.patrickschiller.sourcebraid.share` with identifiers owned by your team. -6. Keep the App Group identical in both targets: `group.de.patrickschiller.sourcebraid`. +5. The existing TestFlight app keeps its pre-rebrand bundle identifiers `de.patrickschiller.stowmark` and `de.patrickschiller.stowmark.share`; changing them would create a different app instead of an update. +6. Keep the pre-rebrand App Group identical in both targets: `group.de.patrickschiller.stowmark`. This preserves settings and Keychain access across the SourceBraid rename. 7. Run the SourceBraid app on your iPhone once and configure GitHub. The fine-grained GitHub token needs `Contents: Read and write` for the private SourceBraid repository. It is stored in a Keychain access group shared only by the app and extension. @@ -38,8 +38,9 @@ xcodebuild \ -allowProvisioningUpdates ``` -The export options upload the archive directly to App Store Connect. The app -record there must use bundle ID `de.patrickschiller.sourcebraid`. +The export options upload the archive directly to App Store Connect. The existing +app record uses bundle ID `de.patrickschiller.stowmark`; its customer-facing name +is **SourceBraid**. ## Use @@ -49,7 +50,7 @@ In FAZ, Safari, Files, or another app: 2. Choose **SourceBraid**. If it is hidden, use **More** to enable it. 3. Edit the title, add optional tags or a note, and tap **Save**. -Apps such as Chrome and FAZ generally share only a URL. SourceBraid loads public web URLs in an isolated web view and converts the readable page content to Markdown before saving. Safari can additionally supply its already visible page text through the extension's preprocessing script. Pages that require an authenticated browser session or block the isolated request are saved as clearly labeled link-only clips. +Apps such as Chrome and FAZ generally share only a URL. SourceBraid loads public web URLs in an isolated web view and converts the readable page content to Markdown before saving, recording the same extraction methods as the browser extension (including specialized DeepMind captures). Safari can additionally supply its already visible page text through the extension's preprocessing script. Shared PDFs are queued as `pdf-docling-pending`; the PDF is pushed last so the existing GitHub Actions workflow can safely convert it after its Markdown and index metadata exist. Pages that require an authenticated browser session or block the isolated request are saved as clearly labeled link-only clips. ## Build without signing diff --git a/ios/SourceBraid.xcodeproj/project.pbxproj b/ios/SourceBraid.xcodeproj/project.pbxproj index 3f1cf7d..e99fa28 100644 --- a/ios/SourceBraid.xcodeproj/project.pbxproj +++ b/ios/SourceBraid.xcodeproj/project.pbxproj @@ -451,7 +451,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = SourceBraid/Resources/SourceBraid.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = H76K2HQUFB; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; @@ -462,7 +462,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.2.0; - PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid; + PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -480,7 +480,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = SourceBraid/Resources/SourceBraid.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = H76K2HQUFB; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = SourceBraid/Resources/Info.plist; @@ -490,7 +490,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.2.0; - PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid; + PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -507,7 +507,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = SourceBraidShare/SourceBraidShare.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = H76K2HQUFB; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = SourceBraidShare/Info.plist; @@ -516,7 +516,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 0.2.0; - PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid.share; + PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark.share; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -534,7 +534,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = SourceBraidShare/SourceBraidShare.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = H76K2HQUFB; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = SourceBraidShare/Info.plist; @@ -543,7 +543,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 0.2.0; - PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid.share; + PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark.share; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -566,7 +566,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid.tests; + PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = iphonesimulator; SWIFT_VERSION = 5.0; @@ -586,7 +586,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid.tests; + PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = iphonesimulator; SWIFT_VERSION = 5.0; diff --git a/ios/SourceBraid/Resources/Info.plist b/ios/SourceBraid/Resources/Info.plist index a46768c..4bdbf0d 100644 --- a/ios/SourceBraid/Resources/Info.plist +++ b/ios/SourceBraid/Resources/Info.plist @@ -23,7 +23,7 @@ ITSAppUsesNonExemptEncryption SourceBraidKeychainAccessGroup - $(AppIdentifierPrefix)de.patrickschiller.sourcebraid.shared + $(AppIdentifierPrefix)de.patrickschiller.stowmark.shared UILaunchScreen UISupportedInterfaceOrientations diff --git a/ios/SourceBraid/Resources/SourceBraid.entitlements b/ios/SourceBraid/Resources/SourceBraid.entitlements index e6d1349..3e1e535 100644 --- a/ios/SourceBraid/Resources/SourceBraid.entitlements +++ b/ios/SourceBraid/Resources/SourceBraid.entitlements @@ -4,11 +4,11 @@ com.apple.security.application-groups - group.de.patrickschiller.sourcebraid + group.de.patrickschiller.stowmark keychain-access-groups - $(AppIdentifierPrefix)de.patrickschiller.sourcebraid.shared + $(AppIdentifierPrefix)de.patrickschiller.stowmark.shared diff --git a/ios/SourceBraid/Shared/ClipBuilder.swift b/ios/SourceBraid/Shared/ClipBuilder.swift index 4e129a0..2da1a7e 100644 --- a/ios/SourceBraid/Shared/ClipBuilder.swift +++ b/ios/SourceBraid/Shared/ClipBuilder.swift @@ -45,16 +45,22 @@ struct SourceBraidIndexEntry: Encodable { let captureMethod: String let sourceType: String let contentFormat: String + let conversionStatus: String? + let converter: String? let capturedAt: String let attachmentPath: String? + let pdfPath: String? enum CodingKeys: String, CodingKey { case title, url, path, date, tags, source case captureMethod = "capture_method" case sourceType = "source_type" case contentFormat = "content_format" + case conversionStatus = "conversion_status" + case converter case capturedAt = "captured_at" case attachmentPath = "attachment_path" + case pdfPath = "pdf_path" } } @@ -90,11 +96,16 @@ enum ClipBuilder { let captureMethod: String let sourceType: String let contentFormat: String + let queuesPDFConversion = input.fileData != nil && input.mimeType == "application/pdf" - if input.fileData != nil { + if queuesPDFConversion { + captureMethod = "pdf-docling-pending" + sourceType = "pdf" + contentFormat = "pdf" + } else if input.fileData != nil { captureMethod = "ios-share-file" sourceType = "document" - contentFormat = input.mimeType == "application/pdf" ? "pdf" : "file" + contentFormat = "file" } else if !input.articleText.trimmed.isEmpty { captureMethod = input.articleCaptureMethod ?? "ios-share-safari" sourceType = "article" @@ -113,7 +124,8 @@ enum ClipBuilder { if let data = input.fileData { let ext = safeFileExtension(input.filename, mimeType: input.mimeType) let clipSlug = path.split(separator: "/").last.map(String.init)?.replacingOccurrences(of: ".md", with: "") ?? "document" - let attachmentPath = "\(normalizedConfiguration.rootFolder)/\(captureDate.prefix(4))/\(captureDate.dropFirst(5).prefix(2))/assets/\(clipSlug)/original.\(ext)" + let attachmentName = queuesPDFConversion ? "source.pdf" : "original.\(ext)" + let attachmentPath = "\(normalizedConfiguration.rootFolder)/\(captureDate.prefix(4))/\(captureDate.dropFirst(5).prefix(2))/assets/\(clipSlug)/\(attachmentName)" attachment = CaptureAttachment(path: attachmentPath, data: data) } else { attachment = nil @@ -129,6 +141,7 @@ enum ClipBuilder { captureMethod: captureMethod, sourceType: sourceType, contentFormat: contentFormat, + queuesPDFConversion: queuesPDFConversion, tags: tags, notes: notes, attachmentPath: attachment?.path, @@ -144,8 +157,11 @@ enum ClipBuilder { captureMethod: captureMethod, sourceType: sourceType, contentFormat: contentFormat, + conversionStatus: queuesPDFConversion ? "pending" : nil, + converter: queuesPDFConversion ? "docling" : nil, capturedAt: capturedAt, - attachmentPath: attachment?.path + attachmentPath: queuesPDFConversion ? nil : attachment?.path, + pdfPath: queuesPDFConversion ? attachment?.path : nil ) return CaptureDraft(title: title, path: path, markdown: markdown, indexEntry: entry, attachment: attachment) } @@ -168,6 +184,7 @@ enum ClipBuilder { captureMethod: String, sourceType: String, contentFormat: String, + queuesPDFConversion: Bool, tags: [String], notes: String, attachmentPath: String?, @@ -184,7 +201,11 @@ enum ClipBuilder { "source_type: \(yamlQuote(sourceType))", "content_format: \(yamlQuote(contentFormat))" ] - if let attachmentPath { + if queuesPDFConversion, let attachmentPath { + lines.append("conversion_status: \(yamlQuote("pending"))") + lines.append("converter: \(yamlQuote("docling"))") + lines.append("pdf_path: \(yamlQuote(attachmentPath))") + } else if let attachmentPath { lines.append("attachment_path: \(yamlQuote(attachmentPath))") } if !tags.isEmpty { @@ -208,7 +229,9 @@ enum ClipBuilder { if let url = input.url { lines.append(contentsOf: ["[Open original source](\(url.absoluteString))", ""]) } - if let attachmentPath { + if queuesPDFConversion { + lines.append(contentsOf: ["> The original PDF has been uploaded. GitHub Actions will replace this placeholder with Docling Markdown.", ""]) + } else if let attachmentPath { lines.append(contentsOf: ["[Open saved attachment](\(relativePath(from: markdownPath, to: attachmentPath)))", ""]) } diff --git a/ios/SourceBraid/Shared/GitHubClient.swift b/ios/SourceBraid/Shared/GitHubClient.swift index 5a6b7fe..1d0004e 100644 --- a/ios/SourceBraid/Shared/GitHubClient.swift +++ b/ios/SourceBraid/Shared/GitHubClient.swift @@ -14,6 +14,13 @@ struct GitHubClient { } func save(_ draft: CaptureDraft) async throws { + if draft.indexEntry.captureMethod == "pdf-docling-pending", let attachment = draft.attachment { + // The PDF push triggers the conversion workflow, so its metadata must exist first. + try await putReplacing(path: draft.path, data: Data(draft.markdown.utf8), message: "Queue PDF SourceBraid capture: \(draft.title)") + try await updateIndex(with: draft.indexEntry) + try await putReplacing(path: attachment.path, data: attachment.data, message: "Queue SourceBraid PDF: \(draft.title)") + return + } if let attachment = draft.attachment { try await putReplacing(path: attachment.path, data: attachment.data, message: "Save SourceBraid attachment: \(draft.title)") } diff --git a/ios/SourceBraid/Shared/SourceBraidEnvironment.swift b/ios/SourceBraid/Shared/SourceBraidEnvironment.swift index e731a2f..4c51561 100644 --- a/ios/SourceBraid/Shared/SourceBraidEnvironment.swift +++ b/ios/SourceBraid/Shared/SourceBraidEnvironment.swift @@ -1,8 +1,9 @@ import Foundation enum SourceBraidEnvironment { - static let appGroupIdentifier = "group.de.patrickschiller.sourcebraid" - static let keychainService = "de.patrickschiller.sourcebraid" + // Keep the pre-rebrand identifiers so TestFlight upgrades retain settings and credentials. + static let appGroupIdentifier = "group.de.patrickschiller.stowmark" + static let keychainService = "de.patrickschiller.stowmark" static let keychainAccount = "github-token" static let recentCapturesKey = "recent-captures" diff --git a/ios/SourceBraidShare/Info.plist b/ios/SourceBraidShare/Info.plist index 79e9158..a0553d2 100644 --- a/ios/SourceBraidShare/Info.plist +++ b/ios/SourceBraidShare/Info.plist @@ -19,7 +19,7 @@ CFBundleVersion $(CURRENT_PROJECT_VERSION) SourceBraidKeychainAccessGroup - $(AppIdentifierPrefix)de.patrickschiller.sourcebraid.shared + $(AppIdentifierPrefix)de.patrickschiller.stowmark.shared NSExtension NSExtensionAttributes diff --git a/ios/SourceBraidShare/ShareViewModel.swift b/ios/SourceBraidShare/ShareViewModel.swift index 3051e8b..7630dc2 100644 --- a/ios/SourceBraidShare/ShareViewModel.swift +++ b/ios/SourceBraidShare/ShareViewModel.swift @@ -83,7 +83,7 @@ final class ShareViewModel: ObservableObject { let result = try await capture.capture(url: url) input.articleText = result.markdown input.articleContentFormat = "markdown" - input.articleCaptureMethod = "ios-share-web" + input.articleCaptureMethod = result.captureMethod if input.suggestedTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { input.suggestedTitle = result.title } diff --git a/ios/SourceBraidShare/SourceBraidShare.entitlements b/ios/SourceBraidShare/SourceBraidShare.entitlements index e6d1349..3e1e535 100644 --- a/ios/SourceBraidShare/SourceBraidShare.entitlements +++ b/ios/SourceBraidShare/SourceBraidShare.entitlements @@ -4,11 +4,11 @@ com.apple.security.application-groups - group.de.patrickschiller.sourcebraid + group.de.patrickschiller.stowmark keychain-access-groups - $(AppIdentifierPrefix)de.patrickschiller.sourcebraid.shared + $(AppIdentifierPrefix)de.patrickschiller.stowmark.shared diff --git a/ios/SourceBraidShare/WebPageCapture.js b/ios/SourceBraidShare/WebPageCapture.js index 010b136..4c88669 100644 --- a/ios/SourceBraidShare/WebPageCapture.js +++ b/ios/SourceBraidShare/WebPageCapture.js @@ -176,8 +176,33 @@ return candidates.sort(function (left, right) { return rootScore(right) - rootScore(left); })[0]; } - var root = chooseRoot(); - if (!root) return JSON.stringify({ title: document.title || "", url: location.href, markdown: "" }); + function chooseGoogleDeepMindRoot() { + if (location.hostname.toLowerCase() !== "deepmind.google" || location.pathname.indexOf("/blog/") !== 0) { + return null; + } + var main = document.querySelector("main"); + if (!main) return null; + + var content = document.createElement("div"); + Array.from(main.children).some(function (section) { + if (section.tagName !== "SECTION") return false; + var heading = cleanText((section.querySelector("h1, h2, h3") || {}).textContent || ""); + if (/^related posts$/i.test(heading)) return true; + if (section.matches(".section-cover") || section.querySelector("h1")) return false; + var clone = section.cloneNode(true); + clone.querySelectorAll([ + "script", "style", "nav", "aside", "form", "iframe", "noscript", "svg", "button", "[aria-label='Share']" + ].join(",")).forEach(function (node) { node.remove(); }); + if (cleanText(clone.textContent).length >= 40 || clone.querySelector("img, video")) content.appendChild(clone); + return false; + }); + return cleanText(content.textContent).length >= 500 ? content : null; + } + + var specializedRoot = chooseGoogleDeepMindRoot(); + var root = specializedRoot || chooseRoot(); + var captureMethod = specializedRoot ? "google-deepmind-dom" : "dom-readable"; + if (!root) return JSON.stringify({ title: document.title || "", url: location.href, markdown: "", captureMethod: captureMethod }); var clone = root.cloneNode(true); clone.querySelectorAll([ @@ -198,6 +223,7 @@ return JSON.stringify({ title: title, url: location.href, - markdown: markdown + markdown: markdown, + captureMethod: captureMethod }); })(); diff --git a/ios/SourceBraidShare/WebPageCapture.swift b/ios/SourceBraidShare/WebPageCapture.swift index 1e3da7b..373b10a 100644 --- a/ios/SourceBraidShare/WebPageCapture.swift +++ b/ios/SourceBraidShare/WebPageCapture.swift @@ -5,6 +5,7 @@ struct WebPageCaptureResult: Decodable { let title: String let url: String let markdown: String + let captureMethod: String } @MainActor diff --git a/ios/SourceBraidTests/ClipBuilderTests.swift b/ios/SourceBraidTests/ClipBuilderTests.swift index cf1effd..a776202 100644 --- a/ios/SourceBraidTests/ClipBuilderTests.swift +++ b/ios/SourceBraidTests/ClipBuilderTests.swift @@ -56,9 +56,16 @@ final class ClipBuilderTests: XCTestCase { ) XCTAssertEqual(draft.attachment?.data, Data("pdf".utf8)) - XCTAssertEqual(draft.attachment?.path.split(separator: ".").last, "pdf") - XCTAssertTrue(draft.markdown.contains("[Open saved attachment](assets/")) + XCTAssertTrue(draft.attachment?.path.hasSuffix("/source.pdf") == true) + XCTAssertTrue(draft.markdown.contains("capture_method: \"pdf-docling-pending\"")) XCTAssertTrue(draft.markdown.contains("content_format: \"pdf\"")) + XCTAssertTrue(draft.markdown.contains("conversion_status: \"pending\"")) + XCTAssertTrue(draft.markdown.contains("converter: \"docling\"")) + XCTAssertTrue(draft.markdown.contains("pdf_path: \"") && draft.markdown.contains("/source.pdf\"")) + XCTAssertEqual(draft.indexEntry.captureMethod, "pdf-docling-pending") + XCTAssertEqual(draft.indexEntry.conversionStatus, "pending") + XCTAssertEqual(draft.indexEntry.converter, "docling") + XCTAssertEqual(draft.indexEntry.pdfPath, draft.attachment?.path) } func testFetchedWebCaptureStoresMarkdownMetadataAndBody() throws { @@ -68,7 +75,7 @@ final class ClipBuilderTests: XCTestCase { sharedText: "https://blog.example.com/article", articleText: "## Introduction\n\nA [useful link](https://example.com).", articleContentFormat: "markdown", - articleCaptureMethod: "ios-share-web", + articleCaptureMethod: "dom-readable", fileData: nil, filename: nil, mimeType: nil @@ -83,7 +90,7 @@ final class ClipBuilderTests: XCTestCase { now: date ) - XCTAssertTrue(draft.markdown.contains("capture_method: \"ios-share-web\"")) + XCTAssertTrue(draft.markdown.contains("capture_method: \"dom-readable\"")) XCTAssertTrue(draft.markdown.contains("content_format: \"markdown\"")) XCTAssertTrue(draft.markdown.contains("## Captured content\n\n## Introduction")) XCTAssertFalse(draft.markdown.contains("## Shared text"))