From 6ae4852701b8d8ad4b7116f39002f1447f4af37f Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 21 Apr 2025 20:23:45 +0200 Subject: [PATCH 1/4] Add DeepLinkService to track external ZIM file launch of the app --- App/App_iOS.swift | 10 +++++-- App/SplitViewController.swift | 6 +++- Model/DeepLinkService.swift | 42 ++++++++++++++++++++++++++++ SwiftUI/Model/Enum.swift | 8 +++--- ViewModel/NavigationViewModel.swift | 2 ++ Views/ViewModifiers/FileImport.swift | 10 +++++-- 6 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 Model/DeepLinkService.swift diff --git a/App/App_iOS.swift b/App/App_iOS.swift index c45e050d..778ee976 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -28,6 +28,7 @@ struct Kiwix: App { @StateObject private var navigation = NavigationViewModel() @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate private let fileMonitor: DirectoryMonitor +// @State var isOpeningDeeplink = true init() { fileMonitor = DirectoryMonitor(url: URL.documentDirectory) { LibraryOperations.scanDirectory($0) } @@ -60,14 +61,15 @@ struct Kiwix: App { } case .background: break -// reScheduleBackgroundDownloadTask() @unknown default: break } } .onOpenURL { url in if url.isFileURL { - NotificationCenter.openFiles([url], context: .file) + let deepLinkId = UUID() + DeepLinkService.shared.startFor(uuid: deepLinkId) + NotificationCenter.openFiles([url], context: .file(deepLinkId: deepLinkId)) } else if url.isZIMURL { NotificationCenter.openURL(url) } @@ -77,7 +79,9 @@ struct Kiwix: App { case .kiwix: fileMonitor.start() await LibraryOperations.reopen() - navigation.navigateToMostRecentTab() + if !DeepLinkService.shared.isRunning() { + navigation.navigateToMostRecentTab() + } LibraryOperations.scanDirectory(URL.documentDirectory) LibraryOperations.applyFileBackupSetting() DownloadService.shared.restartHeartbeatIfNeeded() diff --git a/App/SplitViewController.swift b/App/SplitViewController.swift index 8ba489ff..40ab29ae 100644 --- a/App/SplitViewController.swift +++ b/App/SplitViewController.swift @@ -89,7 +89,7 @@ final class SplitViewController: UISplitViewController { }) openURLObserver = NotificationCenter.default.addObserver( - forName: .openURL, object: nil, queue: nil + forName: .openURL, object: nil, queue: .main ) { [weak self] notification in guard let url = notification.userInfo?["url"] as? URL else { return } let inNewTab = notification.userInfo?["inNewTab"] as? Bool ?? false @@ -99,6 +99,10 @@ final class SplitViewController: UISplitViewController { } else if let tabID = self?.navigationViewModel.createTab() { BrowserViewModel.getCached(tabID: tabID).load(url: url) } + if let context = notification.userInfo?["context"] as? OpenURLContext, + case .deepLink(let deepLinkId) = context { + DeepLinkService.shared.stopFor(uuid: deepLinkId) + } } } observeGoBackAndForward() diff --git a/Model/DeepLinkService.swift b/Model/DeepLinkService.swift new file mode 100644 index 00000000..b8d01b0a --- /dev/null +++ b/Model/DeepLinkService.swift @@ -0,0 +1,42 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import Foundation + +/// Helper to figure out if a deeplink started ZIM file +/// handling is already running. +/// In that case we do not want to handle the default +/// navigation to the latest opened ZIM file +@MainActor +final class DeepLinkService { + + static let shared = DeepLinkService() + + private var ids = Set() + + private init() {} + + func startFor(uuid: UUID) { + ids.insert(uuid) + } + + func stopFor(uuid: UUID) { + ids.remove(uuid) + } + + func isRunning() -> Bool { + !ids.isEmpty + } +} diff --git a/SwiftUI/Model/Enum.swift b/SwiftUI/Model/Enum.swift index 14eeaf6d..cc727953 100644 --- a/SwiftUI/Model/Enum.swift +++ b/SwiftUI/Model/Enum.swift @@ -138,14 +138,14 @@ enum ExternalLinkLoadingPolicy: String, CaseIterable, Identifiable, Defaults.Ser } } -enum OpenURLContext: String { - case deepLink +enum OpenURLContext { + case deepLink(id: UUID) case file } -enum OpenFileContext: String { +enum OpenFileContext { case command - case file + case file(deepLinkId: UUID? = nil) case welcomeScreen case library } diff --git a/ViewModel/NavigationViewModel.swift b/ViewModel/NavigationViewModel.swift index 7ebd7276..7a6d45ae 100644 --- a/ViewModel/NavigationViewModel.swift +++ b/ViewModel/NavigationViewModel.swift @@ -16,6 +16,7 @@ import CoreData import WebKit import Combine +import os @MainActor final class NavigationViewModel: ObservableObject { @@ -67,6 +68,7 @@ final class NavigationViewModel: ObservableObject { let tab = (try? context.fetch(fetchRequest).first) ?? Self.makeTab(context: context) Task { await MainActor.run { + os_log("open navigate to most recent tab", log: Log.LibraryOperations, type: .error) currentItem = NavigationItem.tab(objectID: tab.objectID) } } diff --git a/Views/ViewModifiers/FileImport.swift b/Views/ViewModifiers/FileImport.swift index 4eed417e..a0539cc8 100644 --- a/Views/ViewModifiers/FileImport.swift +++ b/Views/ViewModifiers/FileImport.swift @@ -54,14 +54,14 @@ struct OpenFileHandler: ViewModifier { @State private var isAlertPresented = false @State private var activeAlert: ActiveAlert? - private let importFiles = NotificationCenter.default.publisher(for: .openFiles) + private let openFiles = NotificationCenter.default.publisher(for: .openFiles) enum ActiveAlert { case unableToOpen(filenames: [String]) } // swiftlint:disable:next cyclomatic_complexity func body(content: Content) -> some View { - content.onReceive(importFiles) { notification in + content.onReceive(openFiles) { notification in guard let urls = notification.userInfo?["urls"] as? [URL], let context = notification.userInfo?["context"] as? OpenFileContext else { return } @@ -90,7 +90,11 @@ struct OpenFileHandler: ViewModifier { NotificationCenter.openURL(url, inNewTab: true, context: .file) } #elseif os(iOS) - NotificationCenter.openURL(url, inNewTab: true) + if case .file(.some(let deepLinkID)) = context { + NotificationCenter.openURL(url, inNewTab: true, context: .deepLink(id: deepLinkID)) + } else { + NotificationCenter.openURL(url, inNewTab: true) + } #endif } case .welcomeScreen: From a97eb58b0c2ed5589e53e407cdac3213df1c22b7 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 21 Apr 2025 20:32:35 +0200 Subject: [PATCH 2/4] Fixlint --- App/SplitViewController.swift | 17 +++++++++++------ Views/ViewModifiers/FileImport.swift | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/App/SplitViewController.swift b/App/SplitViewController.swift index 40ab29ae..481293da 100644 --- a/App/SplitViewController.swift +++ b/App/SplitViewController.swift @@ -45,7 +45,6 @@ final class SplitViewController: UISplitViewController { } // MARK: - Lifecycle - override func viewDidLoad() { super.viewDidLoad() @@ -63,6 +62,13 @@ final class SplitViewController: UISplitViewController { setSecondaryController() // observers + observeNavigation() + observeOpeningFiles() + observeGoBackAndForward() + observeAppBackgrounding() + } + + private func observeNavigation() { navigationItemObserver = navigationViewModel.$currentItem .receive(on: DispatchQueue.main) // needed to postpones sink after navigationViewModel.currentItem updates .dropFirst() @@ -77,8 +83,7 @@ final class SplitViewController: UISplitViewController { self?.preferredDisplayMode = .automatic } } - showDownloadsObserver = navigationViewModel - .showDownloads + showDownloadsObserver = navigationViewModel.showDownloads .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] _ in if self?.traitCollection.horizontalSizeClass == .regular, @@ -87,7 +92,9 @@ final class SplitViewController: UISplitViewController { } // the compact one is triggered in CompactViewController }) - + } + + private func observeOpeningFiles() { openURLObserver = NotificationCenter.default.addObserver( forName: .openURL, object: nil, queue: .main ) { [weak self] notification in @@ -105,8 +112,6 @@ final class SplitViewController: UISplitViewController { } } } - observeGoBackAndForward() - observeAppBackgrounding() } private func observeGoBackAndForward() { diff --git a/Views/ViewModifiers/FileImport.swift b/Views/ViewModifiers/FileImport.swift index a0539cc8..578f779e 100644 --- a/Views/ViewModifiers/FileImport.swift +++ b/Views/ViewModifiers/FileImport.swift @@ -59,7 +59,7 @@ struct OpenFileHandler: ViewModifier { enum ActiveAlert { case unableToOpen(filenames: [String]) } - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length func body(content: Content) -> some View { content.onReceive(openFiles) { notification in guard let urls = notification.userInfo?["urls"] as? [URL], From fa15632cfa336ee9dd5ed93f79ad2e03486b6d4b Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 21 Apr 2025 20:42:51 +0200 Subject: [PATCH 3/4] Clean up --- App/App_iOS.swift | 3 +-- ViewModel/NavigationViewModel.swift | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/App/App_iOS.swift b/App/App_iOS.swift index 778ee976..8d12abe5 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -28,7 +28,6 @@ struct Kiwix: App { @StateObject private var navigation = NavigationViewModel() @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate private let fileMonitor: DirectoryMonitor -// @State var isOpeningDeeplink = true init() { fileMonitor = DirectoryMonitor(url: URL.documentDirectory) { LibraryOperations.scanDirectory($0) } @@ -80,7 +79,7 @@ struct Kiwix: App { fileMonitor.start() await LibraryOperations.reopen() if !DeepLinkService.shared.isRunning() { - navigation.navigateToMostRecentTab() + navigation.navigateToMostRecentTab() } LibraryOperations.scanDirectory(URL.documentDirectory) LibraryOperations.applyFileBackupSetting() diff --git a/ViewModel/NavigationViewModel.swift b/ViewModel/NavigationViewModel.swift index 7a6d45ae..7ebd7276 100644 --- a/ViewModel/NavigationViewModel.swift +++ b/ViewModel/NavigationViewModel.swift @@ -16,7 +16,6 @@ import CoreData import WebKit import Combine -import os @MainActor final class NavigationViewModel: ObservableObject { @@ -68,7 +67,6 @@ final class NavigationViewModel: ObservableObject { let tab = (try? context.fetch(fetchRequest).first) ?? Self.makeTab(context: context) Task { await MainActor.run { - os_log("open navigate to most recent tab", log: Log.LibraryOperations, type: .error) currentItem = NavigationItem.tab(objectID: tab.objectID) } } From af792edc90d89efcc82f11317b4256b23aaf2fc3 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 21 Apr 2025 21:02:33 +0200 Subject: [PATCH 4/4] Fix up macOS after deeplink changes --- App/App_macOS.swift | 6 ++++-- App/SplitViewController.swift | 2 +- SwiftUI/Model/Enum.swift | 2 +- Views/ViewModifiers/FileImport.swift | 7 +++++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 01e956bf..1b226efe 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -264,12 +264,14 @@ struct RootView: View { // from opening an external file let browser = BrowserViewModel.getCached(tabID: navigation.currentTabId) browser.forceLoadingState() - NotificationCenter.openFiles([url], context: .file) + // deeplink id is not needed on macOS + NotificationCenter.openFiles([url], context: .file(deepLinkId: nil)) } else if url.isZIMURL { // from deeplinks let browser = BrowserViewModel.getCached(tabID: navigation.currentTabId) browser.forceLoadingState() - NotificationCenter.openURL(url, context: .deepLink) + // deeplink id is not needed on macOS + NotificationCenter.openURL(url, context: .deepLink(id: nil)) } } .onReceive(openURL) { notification in diff --git a/App/SplitViewController.swift b/App/SplitViewController.swift index 481293da..f11506ed 100644 --- a/App/SplitViewController.swift +++ b/App/SplitViewController.swift @@ -107,7 +107,7 @@ final class SplitViewController: UISplitViewController { BrowserViewModel.getCached(tabID: tabID).load(url: url) } if let context = notification.userInfo?["context"] as? OpenURLContext, - case .deepLink(let deepLinkId) = context { + case .deepLink(.some(let deepLinkId)) = context { DeepLinkService.shared.stopFor(uuid: deepLinkId) } } diff --git a/SwiftUI/Model/Enum.swift b/SwiftUI/Model/Enum.swift index cc727953..1367758d 100644 --- a/SwiftUI/Model/Enum.swift +++ b/SwiftUI/Model/Enum.swift @@ -139,7 +139,7 @@ enum ExternalLinkLoadingPolicy: String, CaseIterable, Identifiable, Defaults.Ser } enum OpenURLContext { - case deepLink(id: UUID) + case deepLink(id: UUID?) case file } diff --git a/Views/ViewModifiers/FileImport.swift b/Views/ViewModifiers/FileImport.swift index 578f779e..90e6a592 100644 --- a/Views/ViewModifiers/FileImport.swift +++ b/Views/ViewModifiers/FileImport.swift @@ -83,11 +83,14 @@ struct OpenFileHandler: ViewModifier { for fileID in openedZimFileIDs { guard let url = await ZimFileService.shared.getMainPageURL(zimFileID: fileID) else { return } #if os(macOS) - if .command == context { + switch context { + case .command: NotificationCenter.openURL(url, inNewTab: true) - } else if .file == context { + case .file: // Note: inNewTab:true/false has no meaning here, the system will open a new window anyway NotificationCenter.openURL(url, inNewTab: true, context: .file) + default: + break } #elseif os(iOS) if case .file(.some(let deepLinkID)) = context {