-
Notifications
You must be signed in to change notification settings - Fork 47
feat: add URL interception and builtin browser support #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |||||||||
| 4E55BE8A29D45FC00044389C /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4E55BE8929D45FC00044389C /* Kingfisher */; }; | ||||||||||
| 4EC32AF029D81863003A3BD4 /* WebView in Frameworks */ = {isa = PBXBuildFile; productRef = 4EC32AEF29D81863003A3BD4 /* WebView */; }; | ||||||||||
| 4EC32AF229D818FC003A3BD4 /* WebBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC32AF129D818FC003A3BD4 /* WebBrowserView.swift */; }; | ||||||||||
| 411D26B534F24940938F91F4 /* InAppBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934528345C354D5F9B11C816 /* InAppBrowserView.swift */; }; | ||||||||||
| 54E545A18422D6843B082DBB /* MyUploadsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A5D85D42DE8EAFC4D1D8577 /* MyUploadsState.swift */; }; | ||||||||||
| 594E4B3FFA5E7AFD238CC1E3 /* RichView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C3C5B4EEA7DB8198A04E9 /* RichView.swift */; }; | ||||||||||
| 5D04BF9726C9FB6E0005F7E3 /* FeedInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D04BF9626C9FB6E0005F7E3 /* FeedInfo.swift */; }; | ||||||||||
|
|
@@ -88,6 +89,7 @@ | |||||||||
| 5D6871862718761800329E73 /* FeedbackHelperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6871852718761800329E73 /* FeedbackHelperView.swift */; }; | ||||||||||
| 5D6871882718764E00329E73 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6871872718764E00329E73 /* AboutView.swift */; }; | ||||||||||
| 5D6AAAAC2691851100F42A13 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6AAAAB2691851100F42A13 /* Utils.swift */; }; | ||||||||||
| E6F855004AFD402D95495FC4 /* URLRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E07C501559C4957B0DEF9D4 /* URLRouter.swift */; }; | ||||||||||
| 5D6AAAAF2692036100F42A13 /* FeedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6AAAAE2692036100F42A13 /* FeedItemView.swift */; }; | ||||||||||
| 5D6B67B726D54BA1003A246B /* UA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6B67B626D54BA1003A246B /* UA.swift */; }; | ||||||||||
| 5D6EE28B2693DC470053B637 /* FeedDetailPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6EE28A2693DC470053B637 /* FeedDetailPage.swift */; }; | ||||||||||
|
|
@@ -167,7 +169,8 @@ | |||||||||
| E212778C30ED41F39D51D70B /* MarkdownRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AB393F14CD383FE0EA98A9 /* MarkdownRenderer.swift */; }; | ||||||||||
| E6BD52539035CEA6C56D3BDF /* HTMLToMarkdownConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C732E2652C92E5D553656A9 /* HTMLToMarkdownConverter.swift */; }; | ||||||||||
| EC3A2A13EC68ED3A8DFA764A /* RenderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3CC744901D9A994CF6ABE7 /* RenderError.swift */; }; | ||||||||||
| F090B4D9D3B115551BEF05B4 /* AsyncImageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E707C08835B71223A7A3359 /* AsyncImageAttachment.swift */; }; | ||||||||||
| F090B4D9D3B115551BEF05B4 /* AsyncImageAttachment.swift in Sources */ = {isa = PBXBuildFile; | ||||||||||
| A1B2C3D4E5F60002 /* InAppBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60001 /* InAppBrowserView.swift */; }; fileRef = 8E707C08835B71223A7A3359 /* AsyncImageAttachment.swift */; }; | ||||||||||
| /* End PBXBuildFile section */ | ||||||||||
|
|
||||||||||
| /* Begin PBXContainerItemProxy section */ | ||||||||||
|
|
@@ -202,6 +205,7 @@ | |||||||||
| 42D7F9F8E0B5EA32CC951FD5 /* CodeBlockAttachment.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CodeBlockAttachment.swift; path = V2er/Sources/RichView/Models/CodeBlockAttachment.swift; sourceTree = "<group>"; }; | ||||||||||
| 44998D879D9842BBB61D639E /* MentionParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MentionParser.swift; path = V2er/Sources/RichView/Utils/MentionParser.swift; sourceTree = "<group>"; }; | ||||||||||
| 4EC32AF129D818FC003A3BD4 /* WebBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebBrowserView.swift; sourceTree = "<group>"; }; | ||||||||||
| 934528345C354D5F9B11C816 /* InAppBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppBrowserView.swift; sourceTree = "<group>"; }; | ||||||||||
| 547AFEBDC601FEDCE3364643 /* RenderConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RenderConfiguration.swift; path = V2er/Sources/RichView/Models/RenderConfiguration.swift; sourceTree = "<group>"; }; | ||||||||||
| 5D04BF9626C9FB6E0005F7E3 /* FeedInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInfo.swift; sourceTree = "<group>"; }; | ||||||||||
| 5D0A513626E0CBFC006F3D9B /* ExploreActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreActions.swift; sourceTree = "<group>"; }; | ||||||||||
|
|
@@ -271,6 +275,7 @@ | |||||||||
| 5D6871852718761800329E73 /* FeedbackHelperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackHelperView.swift; sourceTree = "<group>"; }; | ||||||||||
| 5D6871872718764E00329E73 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; }; | ||||||||||
| 5D6AAAAB2691851100F42A13 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; }; | ||||||||||
| 0E07C501559C4957B0DEF9D4 /* URLRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRouter.swift; sourceTree = "<group>"; }; | ||||||||||
| 5D6AAAAE2692036100F42A13 /* FeedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemView.swift; sourceTree = "<group>"; }; | ||||||||||
| 5D6B67B626D54BA1003A246B /* UA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UA.swift; sourceTree = "<group>"; }; | ||||||||||
| 5D6EE28A2693DC470053B637 /* FeedDetailPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedDetailPage.swift; sourceTree = "<group>"; }; | ||||||||||
|
|
@@ -351,7 +356,8 @@ | |||||||||
| CB3CC744901D9A994CF6ABE7 /* RenderError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RenderError.swift; path = V2er/Sources/RichView/Models/RenderError.swift; sourceTree = "<group>"; }; | ||||||||||
| D6356D706913919766FD0EA5 /* RenderActor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RenderActor.swift; path = V2er/Sources/RichView/Renderers/RenderActor.swift; sourceTree = "<group>"; }; | ||||||||||
| E205F350A3537A3E41B1AFC3 /* RichContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RichContentView.swift; path = V2er/Sources/RichView/Views/RichContentView.swift; sourceTree = "<group>"; }; | ||||||||||
| E43141D64D5C4A65B1700BF8 /* PostscriptItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostscriptItemView.swift; sourceTree = "<group>"; }; | ||||||||||
| E43141D64D5C4A65B1700BF8 /* PostscriptItemView.swift */ = {isa = PBXFileReference; | ||||||||||
| A1B2C3D4E5F60001 /* InAppBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppBrowserView.swift; sourceTree = "<group>"; }; lastKnownFileType = sourcecode.swift; path = PostscriptItemView.swift; sourceTree = "<group>"; }; | ||||||||||
|
Comment on lines
+359
to
+360
|
||||||||||
| E43141D64D5C4A65B1700BF8 /* PostscriptItemView.swift */ = {isa = PBXFileReference; | |
| A1B2C3D4E5F60001 /* InAppBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppBrowserView.swift; sourceTree = "<group>"; }; lastKnownFileType = sourcecode.swift; path = PostscriptItemView.swift; sourceTree = "<group>"; }; | |
| E43141D64D5C4A65B1700BF8 /* PostscriptItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostscriptItemView.swift; sourceTree = "<group>"; }; | |
| A1B2C3D4E5F60001 /* InAppBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppBrowserView.swift; sourceTree = "<group>"; }; |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The View group contains a duplicate reference to InAppBrowserView.swift with two different UUIDs (934528345C354D5F9B11C816 and A1B2C3D4E5F60001). This will cause build errors or unpredictable behavior. Only one file reference should be present.
| A1B2C3D4E5F60001 /* InAppBrowserView.swift */, |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An InAppBrowserView.swift source file entry is incorrectly placed inside the XCConfigurationList section for UITests. This entry should be in the PBXSourcesBuildPhase section instead. This will cause the project file to be invalid.
| A1B2C3D4E5F60002 /* InAppBrowserView.swift in Sources */, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,13 +19,23 @@ class URLRouter { | |
| private static let v2exAltHost = "v2ex.com" | ||
|
|
||
| /// Result of URL interception | ||
| enum InterceptResult { | ||
| enum InterceptResult: Equatable { | ||
| case topic(id: String) // /t/123456 | ||
| case node(name: String) // /go/swift | ||
| case member(username: String) // /member/username | ||
| case external(url: URL) // External URL | ||
| case webview(url: URL) // Internal URL to open in webview | ||
| case invalid // Invalid URL | ||
|
|
||
| /// Whether this result should use native navigation | ||
| var isNativeNavigation: Bool { | ||
| switch self { | ||
| case .topic, .node, .member: | ||
| return true | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - URL Parsing | ||
|
|
@@ -149,32 +159,44 @@ enum NavigationDestination: Hashable { | |
| case tagDetail(name: String) | ||
| } | ||
|
|
||
| // MARK: - UIApplication Extension | ||
| // MARK: - Link Action | ||
|
|
||
| extension UIApplication { | ||
| /// Open URL with smart routing | ||
| /// Represents the action to take when a link is tapped | ||
| enum LinkAction { | ||
| case navigateToTopic(id: String) | ||
| case navigateToUser(username: String) | ||
| case navigateToNode(name: String) | ||
| case openInAppBrowser(url: URL) | ||
| case openInSafariViewController(url: URL) | ||
| } | ||
|
|
||
| /// Determines the appropriate action for a URL based on settings | ||
| class LinkHandler { | ||
| /// Determine what action to take for a given URL | ||
| /// - Parameters: | ||
| /// - url: URL to open | ||
| /// - completion: Optional completion handler | ||
| @MainActor | ||
| func openURL(_ url: URL, completion: ((Bool) -> Void)? = nil) { | ||
| let urlString = url.absoluteString | ||
| let result = URLRouter.parse(urlString) | ||
| /// - url: The URL to handle | ||
| /// - useBuiltinBrowser: Whether to use the builtin browser for external links | ||
| /// - Returns: The action to take | ||
| static func action(for url: URL, useBuiltinBrowser: Bool) -> LinkAction { | ||
| let result = URLRouter.parse(url.absoluteString) | ||
|
|
||
| switch result { | ||
| case .external(let externalUrl): | ||
| // Open external URLs in Safari | ||
| open(externalUrl, options: [:], completionHandler: completion) | ||
|
|
||
| case .webview(let webviewUrl): | ||
| // For now, open in Safari | ||
| // TODO: Implement in-app webview | ||
| open(webviewUrl, options: [:], completionHandler: completion) | ||
|
|
||
| default: | ||
| // For topic, node, member URLs - should be handled by navigation | ||
| // Fall back to Safari if not handled | ||
| open(url, options: [:], completionHandler: completion) | ||
| case .topic(let id): | ||
| return .navigateToTopic(id: id) | ||
| case .member(let username): | ||
| return .navigateToUser(username: username) | ||
| case .node(let name): | ||
| return .navigateToNode(name: name) | ||
| case .external(let externalUrl), .webview(let externalUrl): | ||
| // When builtin browser is enabled, use InAppBrowser | ||
| // When disabled, use SafariViewController to stay in app | ||
| if useBuiltinBrowser { | ||
| return .openInAppBrowser(url: externalUrl) | ||
| } else { | ||
| return .openInSafariViewController(url: externalUrl) | ||
| } | ||
| case .invalid: | ||
| return .openInSafariViewController(url: url) | ||
| } | ||
| } | ||
|
Comment on lines
+180
to
201
|
||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,10 +11,12 @@ import SwiftUI | |
|
|
||
| struct SettingState: FluxState { | ||
| static let imgurClientIdKey = "imgurClientId" | ||
| static let useBuiltinBrowserKey = "useBuiltinBrowser" | ||
|
|
||
| var appearance: AppearanceMode = .system | ||
| var autoCheckin: Bool = false | ||
| var imgurClientId: String = "" | ||
| var useBuiltinBrowser: Bool = false | ||
|
|
||
| // Checkin state | ||
| var isCheckingIn: Bool = false | ||
|
|
@@ -38,6 +40,8 @@ struct SettingState: FluxState { | |
| self.checkinDays = UserDefaults.standard.integer(forKey: "checkinDays") | ||
| // Load Imgur client ID | ||
| self.imgurClientId = UserDefaults.standard.string(forKey: Self.imgurClientIdKey) ?? "" | ||
| // Load builtin browser preference | ||
| self.useBuiltinBrowser = UserDefaults.standard.bool(forKey: Self.useBuiltinBrowserKey) | ||
| } | ||
|
|
||
| static func saveImgurClientId(_ clientId: String) { | ||
|
|
@@ -128,6 +132,11 @@ struct SettingActions { | |
| let enabled: Bool | ||
| } | ||
|
|
||
| struct ToggleBuiltinBrowserAction: Action { | ||
| var target: Reducer = R | ||
| let enabled: Bool | ||
| } | ||
|
Comment on lines
+135
to
+138
|
||
|
|
||
| struct StartAutoCheckinAction: AwaitAction { | ||
| var target: Reducer = R | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The project file contains malformed entries with duplicated file references and corrupted syntax. Line 172-173 has "fileRef" appearing twice in the same entry, and line 359-360 has a PBXFileReference split across lines with duplicated content. Additionally, lines 753-754 show the same file "InAppBrowserView.swift" referenced twice with different UUIDs (934528345C354D5F9B11C816 and A1B2C3D4E5F60001), and line 1464 incorrectly places a source file entry inside an XCConfigurationList section.