diff --git a/App/App_iOS.swift b/App/App_iOS.swift index c45e050d..8d12abe5 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -60,14 +60,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 +78,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/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 8ba489ff..f11506ed 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,9 +92,11 @@ final class SplitViewController: UISplitViewController { } // the compact one is triggered in CompactViewController }) - + } + + private func observeOpeningFiles() { 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,10 +106,12 @@ 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(.some(let deepLinkId)) = context { + DeepLinkService.shared.stopFor(uuid: deepLinkId) + } } } - observeGoBackAndForward() - observeAppBackgrounding() } private func 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..1367758d 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/Views/ViewModifiers/FileImport.swift b/Views/ViewModifiers/FileImport.swift index 4eed417e..90e6a592 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 + // swiftlint:disable:next cyclomatic_complexity function_body_length 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 } @@ -83,14 +83,21 @@ 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) - 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: