diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 59c94ef95..077952daf 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -99,20 +99,24 @@ struct Kiwix: App { struct RootView: View { @Environment(\.controlActiveState) var controlActiveState - @StateObject private var browser = BrowserViewModel() @StateObject private var navigation = NavigationViewModel() @StateObject private var windowTracker = WindowTracker() - private let primaryItems: [NavigationItem] = [.reading, .bookmarks] + private let primaryItems: [NavigationItem] = [.bookmarks] private let libraryItems: [NavigationItem] = [.opened, .categories, .downloads, .new] private let openURL = NotificationCenter.default.publisher(for: .openURL) private let appTerminates = NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification) private let tabCloses = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification) + /// Close other tabs then the ones received + private let keepOnlyTabs = NotificationCenter.default.publisher(for: .keepOnlyTabs) var body: some View { NavigationSplitView { List(selection: $navigation.currentItem) { - ForEach(primaryItems, id: \.self) { navigationItem in + ForEach( + [NavigationItem.tab(objectID: navigation.currentTabId)] + primaryItems, + id: \.self + ) { navigationItem in Label(navigationItem.name, systemImage: navigationItem.icon) } if FeatureFlags.hasLibrary { @@ -128,16 +132,12 @@ struct RootView: View { switch navigation.currentItem { case .loading: LoadingDataView() - case .reading: + case .tab(let tabID): + let browser = BrowserViewModel.getCached(tabID: tabID) BrowserTab().environmentObject(browser) - .withHostingWindow { window in - if let windowNumber = window?.windowNumber { - browser.restoreByWindowNumber(windowNumber: windowNumber, - urlToTabIdConverter: navigation.tabIDFor(url:)) - } else { - if FeatureFlags.hasLibrary == false { - browser.loadMainArticle() - } + .withHostingWindow { [weak browser] _ in + if FeatureFlags.hasLibrary == false { + browser?.loadMainArticle() } } case .bookmarks: @@ -156,10 +156,10 @@ struct RootView: View { } .frame(minWidth: 650, minHeight: 500) .focusedSceneValue(\.navigationItem, $navigation.currentItem) - .environmentObject(navigation) .modifier(AlertHandler()) .modifier(OpenFileHandler()) .modifier(SaveContentHandler()) + .environmentObject(navigation) .onOpenURL { url in if url.isFileURL { NotificationCenter.openFiles([url], context: .file) @@ -168,7 +168,9 @@ struct RootView: View { } } .onReceive(openURL) { notification in - guard let url = notification.userInfo?["url"] as? URL else { return } + guard let url = notification.userInfo?["url"] as? URL else { + return + } if notification.userInfo?["isFileContext"] as? Bool == true { // handle the opened ZIM file from Finder // for which the system opens a new window, @@ -177,16 +179,17 @@ struct RootView: View { // We need to filter it down the the last window // (which is usually not the key window yet at this point), // and load the content only within that - Task { - if windowTracker.isLastWindow() { - browser.load(url: url) + Task { @MainActor [weak navigation] in + if windowTracker.isLastWindow(), let navigation { + BrowserViewModel.getCached(tabID: navigation.currentTabId).load(url: url) } } return } guard controlActiveState == .key else { return } - navigation.currentItem = .reading - browser.load(url: url) + let tabID = navigation.currentTabId + navigation.currentItem = .tab(objectID: tabID) + BrowserViewModel.getCached(tabID: tabID).load(url: url) } .onReceive(tabCloses) { publisher in // closing one window either by CMD+W || red(X) close button @@ -199,11 +202,20 @@ struct RootView: View { // tab closed by app termination return } - if let tabID = browser.tabID { - // tab closed by user - browser.pauseVideoWhenNotInPIP() - navigation.deleteTab(tabID: tabID) + let tabID = navigation.currentTabId + let browser = BrowserViewModel.getCached(tabID: tabID) + // tab closed by user + browser.pauseVideoWhenNotInPIP() + Task { @MainActor [weak browser] in + await browser?.clear() + } + navigation.deleteTab(tabID: tabID) + } + .onReceive(keepOnlyTabs) { notification in + guard let tabsToKeep = notification.userInfo?["tabIds"] as? Set else { + return } + navigation.keepOnlyTabsBy(tabIds: tabsToKeep) } .onReceive(appTerminates) { _ in // CMD+Q -> Quit Kiwix, this also closes the last window @@ -212,14 +224,14 @@ struct RootView: View { switch AppType.current { case .kiwix: await LibraryOperations.reopen() - navigation.currentItem = .reading + navigation.currentItem = .tab(objectID: navigation.currentTabId) LibraryOperations.scanDirectory(URL.documentDirectory) LibraryOperations.applyFileBackupSetting() DownloadService.shared.restartHeartbeatIfNeeded() case let .custom(zimFileURL): await LibraryOperations.open(url: zimFileURL) ZimMigration.forCustomApps() - navigation.currentItem = .reading + navigation.currentItem = .tab(objectID: navigation.currentTabId) } // MARK: - migrations if !ProcessInfo.processInfo.arguments.contains("testing") { diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index 5250adb69..0d6984780 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -203,7 +203,7 @@ private struct CompactView: View { case .library: Library(dismiss: dismiss) case .settings: - NavigationView { + NavigationStack { Settings().toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { diff --git a/App/SidebarViewController.swift b/App/SidebarViewController.swift index a7413c372..c263dea55 100644 --- a/App/SidebarViewController.swift +++ b/App/SidebarViewController.swift @@ -33,7 +33,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl collectionView, indexPath, item in collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) } - dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in + dataSource.supplementaryViewProvider = { collectionView, _, indexPath in collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath) } return dataSource @@ -45,6 +45,10 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl cacheName: nil ) + private var navigationViewModel: NavigationViewModel? { + (splitViewController as? SplitViewController)?.navigationViewModel + } + enum Section: String, CaseIterable { case primary case tabs @@ -62,7 +66,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl init() { super.init(collectionViewLayout: UICollectionViewLayout()) - collectionView.collectionViewLayout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in + collectionView.collectionViewLayout = UICollectionViewCompositionalLayout { _, layoutEnvironment in var config = UICollectionLayoutListConfiguration(appearance: .sidebar) config.headerMode = .supplementary config.trailingSwipeActionsConfigurationProvider = { [unowned self] indexPath in @@ -78,8 +82,8 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl } func updateSelection() { - guard let splitViewController = splitViewController as? SplitViewController, - let currentItem = splitViewController.navigationViewModel.currentItem, + guard let navigationViewModel, + let currentItem = navigationViewModel.currentItem, let indexPath = dataSource.indexPath(for: currentItem), collectionView.indexPathsForSelectedItems?.first != indexPath else { return } collectionView.selectItem(at: indexPath, animated: true, scrollPosition: []) @@ -96,8 +100,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl navigationItem.rightBarButtonItem = UIBarButtonItem( image: UIImage(systemName: "plus.square"), primaryAction: UIAction { [unowned self] _ in - guard let splitViewController = splitViewController as? SplitViewController else { return } - splitViewController.navigationViewModel.createTab() + navigationViewModel?.createTab() }, menu: UIMenu(children: [ UIAction( @@ -105,17 +108,16 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl image: UIImage(systemName: "xmark.square"), attributes: .destructive ) { [unowned self] _ in - guard let splitViewController = splitViewController as? SplitViewController, - case let .tab(tabID) = splitViewController.navigationViewModel.currentItem else { return } - splitViewController.navigationViewModel.deleteTab(tabID: tabID) + guard let navigationViewModel, + case let .tab(tabID) = navigationViewModel.currentItem else { return } + navigationViewModel.deleteTab(tabID: tabID) }, UIAction( title: "sidebar_view.navigation.button.close_all".localized, image: UIImage(systemName: "xmark.square.fill"), attributes: .destructive ) { [unowned self] _ in - guard let splitViewController = splitViewController as? SplitViewController else { return } - splitViewController.navigationViewModel.deleteAllTabs() + navigationViewModel?.deleteAllTabs() } ]) ) @@ -148,34 +150,35 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl _ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference ) { - let tabs = snapshot.itemIdentifiers + let tabIds = snapshot.itemIdentifiers .compactMap { $0 as? NSManagedObjectID } - .map { NavigationItem.tab(objectID: $0) } - var snapshot = NSDiffableDataSourceSectionSnapshot() - snapshot.append(tabs) - Task { [snapshot] in - await MainActor.run { [snapshot] in + let tabs = tabIds.map { NavigationItem.tab(objectID: $0) } + var tabsSnapshot = NSDiffableDataSourceSectionSnapshot() + tabsSnapshot.append(tabs) + Task { [tabsSnapshot] in + await MainActor.run { [tabsSnapshot] in dataSource.apply( - snapshot, + tabsSnapshot, to: .tabs, animatingDifferences: dataSource.snapshot(for: .tabs).items.count > 0 ) { guard let indexPath = self.collectionView.indexPathsForSelectedItems?.first, let item = self.dataSource.itemIdentifier(for: indexPath), case .tab = item else { return } - var snapshot = self.dataSource.snapshot() - snapshot.reconfigureItems([item]) - self.dataSource.apply(snapshot, animatingDifferences: true) + var sourceSnapshot = self.dataSource.snapshot() + sourceSnapshot.reconfigureItems([item]) + self.dataSource.apply(sourceSnapshot, animatingDifferences: true) } } } } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let splitViewController = splitViewController as? SplitViewController, + guard let splitViewController, + let navigationViewModel, let navigationItem = dataSource.itemIdentifier(for: indexPath) else { return } - if splitViewController.navigationViewModel.currentItem != navigationItem { - splitViewController.navigationViewModel.currentItem = navigationItem + if navigationViewModel.currentItem != navigationItem { + navigationViewModel.currentItem = navigationItem } if splitViewController.displayMode == .oneOverSecondary { splitViewController.hide(.primary) @@ -232,12 +235,13 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl } private func configureSwipeAction(indexPath: IndexPath) -> UISwipeActionsConfiguration? { - guard let splitViewController = splitViewController as? SplitViewController, + guard let navigationViewModel, let item = dataSource.itemIdentifier(for: indexPath), case let .tab(tabID) = item else { return nil } + let title = "sidebar_view.navigation.button.close".localized let action = UIContextualAction(style: .destructive, - title: "sidebar_view.navigation.button.close".localized) { _, _, _ in - splitViewController.navigationViewModel.deleteTab(tabID: tabID) + title: title) { [weak navigationViewModel] _, _, _ in + navigationViewModel?.deleteTab(tabID: tabID) } action.image = UIImage(systemName: "xmark") return UISwipeActionsConfiguration(actions: [action]) diff --git a/App/SplitViewController.swift b/App/SplitViewController.swift index cefd138e7..c13cfffe9 100644 --- a/App/SplitViewController.swift +++ b/App/SplitViewController.swift @@ -79,11 +79,10 @@ final class SplitViewController: UISplitViewController { ) { [weak self] notification in guard let url = notification.userInfo?["url"] as? URL else { return } let inNewTab = notification.userInfo?["inNewTab"] as? Bool ?? false - Task { @MainActor in + Task { @MainActor [weak self] in if !inNewTab, case let .tab(tabID) = self?.navigationViewModel.currentItem { BrowserViewModel.getCached(tabID: tabID).load(url: url) - } else { - guard let tabID = self?.navigationViewModel.createTab() else { return } + } else if let tabID = self?.navigationViewModel.createTab() { BrowserViewModel.getCached(tabID: tabID).load(url: url) } } @@ -114,7 +113,8 @@ final class SplitViewController: UISplitViewController { let controller = UIHostingController(rootView: Bookmarks()) setViewController(UINavigationController(rootViewController: controller), for: .secondary) case .tab(let tabID): - let view = BrowserTab().environmentObject(BrowserViewModel.getCached(tabID: tabID)) + let view = BrowserTab() + .environmentObject(BrowserViewModel.getCached(tabID: tabID)) let controller = UIHostingController(rootView: view) controller.navigationItem.scrollEdgeAppearance = { let apperance = UINavigationBarAppearance() diff --git a/Model/Entities/Entities.swift b/Model/Entities/Entities.swift index 2aed88af1..585386686 100644 --- a/Model/Entities/Entities.swift +++ b/Model/Entities/Entities.swift @@ -121,7 +121,7 @@ class OutlineItem: ObservableObject, Identifiable { } } -class Tab: NSManagedObject, Identifiable { +final class Tab: NSManagedObject, Identifiable { @NSManaged var created: Date @NSManaged var interactionState: Data? @NSManaged var lastOpened: Date @@ -129,7 +129,7 @@ class Tab: NSManagedObject, Identifiable { @NSManaged var zimFile: ZimFile? - class func fetchRequest( + static func fetchRequest( predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor] = [] ) -> NSFetchRequest { // swiftlint:disable:next force_cast @@ -139,7 +139,7 @@ class Tab: NSManagedObject, Identifiable { return request } - class func fetchRequest(id: UUID) -> NSFetchRequest { + static func fetchRequest(id: UUID) -> NSFetchRequest { // swiftlint:disable:next force_cast let request = super.fetchRequest() as! NSFetchRequest request.predicate = NSPredicate(format: "id == %@", id as CVarArg) @@ -265,7 +265,7 @@ final class ZimFile: NSManagedObject, Identifiable { Predicate.notMissing ]) - class func fetchRequest( + static func fetchRequest( predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor] = [] ) -> NSFetchRequest { // swiftlint:disable:next force_cast @@ -275,7 +275,7 @@ final class ZimFile: NSManagedObject, Identifiable { return request } - class func fetchRequest(fileID: UUID) -> NSFetchRequest { + static func fetchRequest(fileID: UUID) -> NSFetchRequest { // swiftlint:disable:next force_cast let request = super.fetchRequest() as! NSFetchRequest request.predicate = NSPredicate(format: "fileID == %@", fileID as CVarArg) diff --git a/Model/Utilities/URL.swift b/Model/Utilities/URL.swift index cf24c952e..108ae837f 100644 --- a/Model/Utilities/URL.swift +++ b/Model/Utilities/URL.swift @@ -53,6 +53,10 @@ extension URL { var isZIMURL: Bool { schemeType == .zim } var isKiwixURL: Bool { schemeType == .kiwix } var isGeoURL: Bool { schemeType == .geo } + var zimFileID: UUID? { + guard isZIMURL || isKiwixURL else { return nil } + return UUID(uuidString: host ?? "") + } /// Returns the path, that should be used to resolve articles in ZIM files. /// It makes sure that trailing slash is preserved, diff --git a/Model/ZimFileService/ZimFileService.swift b/Model/ZimFileService/ZimFileService.swift index 6bb4ff8ff..ddcf0eb34 100644 --- a/Model/ZimFileService/ZimFileService.swift +++ b/Model/ZimFileService/ZimFileService.swift @@ -92,8 +92,7 @@ } func getRedirectedURL(url: URL) -> URL? { - guard let zimFileID = url.host, - let zimFileID = UUID(uuidString: zimFileID), + guard let zimFileID = url.zimFileID, let redirectedPath = __getRedirectedPath(zimFileID, contentPath: url.contentPath) else { return nil } return URL(zimFileID: zimFileID.uuidString, contentPath: redirectedPath) } @@ -123,14 +122,12 @@ } func getContentSize(url: URL) -> NSNumber? { - guard let zimFileID = url.host, - let zimFileUUID = UUID(uuidString: zimFileID) else { return nil } + guard let zimFileUUID = url.zimFileID else { return nil } return __getContentSize(zimFileUUID, contentPath: url.contentPath) } func getDirectAccessInfo(url: URL) -> DirectAccessInfo? { - guard let zimFileID = url.host, - let zimFileUUID = UUID(uuidString: zimFileID), + guard let zimFileUUID = url.zimFileID, let directAccess = __getDirectAccess(zimFileUUID, contentPath: url.contentPath), let path: String = directAccess["path"] as? String, let offset: UInt = directAccess["offset"] as? UInt @@ -141,8 +138,7 @@ } func getContentMetaData(url: URL) -> URLContentMetaData? { - guard let zimFileID = url.host, - let zimFileUUID = UUID(uuidString: zimFileID), + guard let zimFileUUID = url.zimFileID, let content = __getMetaData(zimFileUUID, contentPath: url.contentPath), let mime = content["mime"] as? String, let size = content["size"] as? UInt, diff --git a/SwiftUI/Model/Enum.swift b/SwiftUI/Model/Enum.swift index 4dd956379..509e9dfc8 100644 --- a/SwiftUI/Model/Enum.swift +++ b/SwiftUI/Model/Enum.swift @@ -141,7 +141,7 @@ enum ExternalLinkLoadingPolicy: String, CaseIterable, Identifiable, Defaults.Ser enum OpenFileContext: String { case command case file - case onBoarding + case welcomeScreen case library } @@ -217,7 +217,7 @@ enum NavigationItem: Hashable, Identifiable { var id: Int { hashValue } case loading - case reading, bookmarks, map(location: CLLocation?), tab(objectID: NSManagedObjectID) + case bookmarks, map(location: CLLocation?), tab(objectID: NSManagedObjectID) case opened, categories, new, downloads case settings @@ -225,14 +225,16 @@ enum NavigationItem: Hashable, Identifiable { switch self { case .loading: return "enum.navigation_item.loading".localized - case .reading: - return "enum.navigation_item.reading".localized case .bookmarks: return "enum.navigation_item.bookmarks".localized case .map: return "enum.navigation_item.map".localized case .tab: + #if os(macOS) + return "enum.navigation_item.reading".localized + #else return "enum.navigation_item.new_tab".localized + #endif case .opened: return "enum.navigation_item.opened".localized case .categories: @@ -250,14 +252,16 @@ enum NavigationItem: Hashable, Identifiable { switch self { case .loading: return "loading" - case .reading: - return "book" case .bookmarks: return "star" case .map: return "map" case .tab: + #if os(macOS) + return "book" + #else return "square" + #endif case .opened: return "folder" case .categories: diff --git a/SwiftUI/Model/LibraryOperations.swift b/SwiftUI/Model/LibraryOperations.swift index 00984aa69..19aa9c45b 100644 --- a/SwiftUI/Model/LibraryOperations.swift +++ b/SwiftUI/Model/LibraryOperations.swift @@ -161,6 +161,27 @@ struct LibraryOperations { zimFile.isMissing = false } zimFile.tabs.forEach { context.delete($0) } + + if let tabs = try? Tab.fetchRequest().execute() { + let tabIds = tabs.map { $0.objectID } + // clear out all the browserViewModels of tabs no longer in use + BrowserViewModel.keepOnlyTabsByIds(Set(tabIds)) + + #if os(iOS) + // make sure we won't end up without any tabs + if tabs.count == 0 { + let tab = Tab(context: context) + tab.created = Date() + tab.lastOpened = Date() + try? context.obtainPermanentIDs(for: [tab]) + } + #else + if context.hasChanges { try? context.save() } + Task { @MainActor in + NotificationCenter.keepOnlyTabs(Set(tabIds)) + } + #endif + } if context.hasChanges { try? context.save() } } } diff --git a/SwiftUI/Patches.swift b/SwiftUI/Patches.swift index bca8e79e4..f15249e8f 100644 --- a/SwiftUI/Patches.swift +++ b/SwiftUI/Patches.swift @@ -74,6 +74,9 @@ extension Notification.Name { static let exportFileData = Notification.Name("exportFileData") static let saveContent = Notification.Name("saveContent") static let toggleSidebar = Notification.Name("toggleSidebar") + #if os(macOS) + static let keepOnlyTabs = Notification.Name("keepOnlyTabs") + #endif } extension UTType { @@ -81,6 +84,7 @@ extension UTType { } extension NotificationCenter { + @MainActor static func openURL(_ url: URL, inNewTab: Bool = false, isFileContext: Bool = false) { NotificationCenter.default.post( name: .openURL, @@ -108,4 +112,12 @@ extension NotificationCenter { static func toggleSidebar() { NotificationCenter.default.post(name: .toggleSidebar, object: nil, userInfo: nil) } + + #if os(macOS) + @MainActor static func keepOnlyTabs(_ tabIds: Set) { + NotificationCenter.default.post(name: .keepOnlyTabs, + object: nil, + userInfo: ["tabIds": tabIds]) + } + #endif } diff --git a/Tests/OrderedCacheTests.swift b/Tests/OrderedCacheTests.swift index 7ea3fd0e8..6163cfbd2 100644 --- a/Tests/OrderedCacheTests.swift +++ b/Tests/OrderedCacheTests.swift @@ -60,4 +60,19 @@ final class OrderedCacheTests: XCTestCase { XCTAssertEqual(cache.findBy(key: "one"), 1) } + @MainActor + func testRemoveByNotMatchingKeys() { + let cache = OrderedCache() + cache.setValue(101, forKey: "one_zero_one") + cache.setValue(1, forKey: "one") + cache.setValue(202, forKey: "two_zero_two") + let removed = cache.removeNotMatchingWith(keys: Set(["some", "one", "else"])) + XCTAssertEqual(cache.count, 1) + XCTAssertNil(cache.findBy(key: "zero")) + XCTAssertEqual(cache.findBy(key: "one"), 1) + XCTAssertEqual(removed.count, 2) + XCTAssertTrue(removed.contains(101)) + XCTAssertTrue(removed.contains(202)) + } + } diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index df6b2156a..444e0035d 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -26,6 +26,7 @@ import CoreKiwix final class BrowserViewModel: NSObject, ObservableObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate, NSFetchedResultsControllerDelegate { + @MainActor private static var cache: OrderedCache? @@ -49,6 +50,16 @@ final class BrowserViewModel: NSObject, ObservableObject, } } + nonisolated static func keepOnlyTabsByIds(_ ids: Set) { + Task { @MainActor in + if let cache { + for browser in cache.removeNotMatchingWith(keys: ids) { + await browser.destroy() + } + } + } + } + // MARK: - Properties @Published private(set) var isLoading: Bool? @@ -72,36 +83,27 @@ final class BrowserViewModel: NSObject, ObservableObject, hasURL = url != nil } } + @MainActor var zimFileId: UUID? { url?.zimFileID } @Published var externalURL: URL? private var metaData: URLContentMetaData? - private(set) var tabID: NSManagedObjectID? { - didSet { -#if os(macOS) - if let tabID, tabID != oldValue { - storeTabIDInCurrentWindow() - } -#endif - } - } #if os(macOS) private var windowURLs: [URL] { UserDefaults.standard[.windowURLs] } #endif let webView: WKWebView + let tabID: NSManagedObjectID private var isLoadingObserver: NSKeyValueObservation? private var canGoBackObserver: NSKeyValueObservation? private var canGoForwardObserver: NSKeyValueObservation? private var titleURLObserver: AnyCancellable? private let bookmarkFetchedResultsController: NSFetchedResultsController - /// A temporary placeholder for the url that should be opened in a new tab, set on macOS only - static var urlForNewTab: URL? // MARK: - Lifecycle // swiftlint:disable:next function_body_length - @MainActor init(tabID: NSManagedObjectID? = nil) { + @MainActor private init(tabID: NSManagedObjectID) { self.tabID = tabID webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) if !Bundle.main.isProduction, #available(iOS 16.4, macOS 13.3, *) { @@ -126,13 +128,7 @@ final class BrowserViewModel: NSObject, ObservableObject, webView.navigationDelegate = self webView.uiDelegate = self - if let tabID { - restoreBy(tabID: tabID) - } - if let urlForNewTab = Self.urlForNewTab { - url = urlForNewTab - load(url: urlForNewTab) - } + restoreBy(tabID: tabID) // get outline items if something is already loaded if webView.url != nil { @@ -173,6 +169,37 @@ final class BrowserViewModel: NSObject, ObservableObject, } } + @MainActor + func destroy() async { + bookmarkFetchedResultsController.delegate = nil + canGoBackObserver?.invalidate() + canGoForwardObserver?.invalidate() + titleURLObserver?.cancel() + isLoadingObserver?.invalidate() + webView.navigationDelegate = nil + webView.uiDelegate = nil + #if os(iOS) + webView.scrollView.delegate = nil + #endif + await clear() + } + + @MainActor + func clear() async { + await webView.setAllMediaPlaybackSuspended(true) + await webView.closeAllMediaPresentations() + webView.stopLoading() + url = nil + articleTitle = "" + zimFileName = "" + outlineItems = [] + outlineItemTree = [] + // !important to make the webView disappear, + // until new content is not starting to load + // as complete clear of webView is not possible + isLoading = nil + } + /// Get the webpage in a binary format /// - Returns: PDF of the current page (if text type) or binary data of the content /// and the file extension, if known @@ -196,7 +223,7 @@ final class BrowserViewModel: NSObject, ObservableObject, private func didUpdate(title: String, url: URL) { let zimFile: ZimFile? = { - guard let zimFileID = UUID(uuidString: url.host ?? "") else { return nil } + guard let zimFileID = url.zimFileID else { return nil } return try? Database.shared.viewContext.fetch(ZimFile.fetchRequest(fileID: zimFileID)).first }() @@ -211,28 +238,30 @@ final class BrowserViewModel: NSObject, ObservableObject, zimFileName = zimFile?.name ?? "" self.url = url - let currentTabID: NSManagedObjectID = tabID ?? createNewTabID() - tabID = currentTabID - // update tab data - if let tab = try? Database.shared.viewContext.existingObject(with: currentTabID) as? Tab { + let context = Database.shared.viewContext + if let tab = try? context.existingObject(with: tabID) as? Tab { tab.title = articleTitle tab.zimFile = zimFile } + if context.hasChanges { + try? context.save() + } #if os(macOS) disableVideoContextMenu() #endif } } + @MainActor func updateLastOpened() { - guard let tabID, let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab else { return } + guard let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab else { return } tab.lastOpened = Date() } + @MainActor func persistState() { - guard let tabID, - let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab else { + guard let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab else { return } tab.interactionState = webView.interactionState as? Data @@ -249,7 +278,7 @@ final class BrowserViewModel: NSObject, ObservableObject, @MainActor func loadRandomArticle(zimFileID: UUID? = nil) { - let zimFileID = zimFileID ?? UUID(uuidString: webView.url?.host ?? "") + let zimFileID = zimFileID ?? webView.url?.zimFileID Task { @ZimActor [weak self] in guard let url = ZimFileService.shared.getRandomPageURL(zimFileID: zimFileID) else { return } await MainActor.run { [weak self] in @@ -260,7 +289,7 @@ final class BrowserViewModel: NSObject, ObservableObject, @MainActor func loadMainArticle(zimFileID: UUID? = nil) { - let zimFileID = zimFileID ?? UUID(uuidString: webView.url?.host ?? "") + let zimFileID = zimFileID ?? webView.url?.zimFileID Task { @ZimActor [weak self] in guard let url = ZimFileService.shared.getMainPageURL(zimFileID: zimFileID) else { return } await MainActor.run { [weak self] in @@ -318,16 +347,30 @@ final class BrowserViewModel: NSObject, ObservableObject, // MARK: - New Tab Creation #if os(macOS) - private func createNewTab(url: URL) -> Bool { - guard let currentWindow = NSApp.keyWindow else { return false } - guard let windowController = currentWindow.windowController else { return false } - // store the new url in a static way - BrowserViewModel.urlForNewTab = url - // this creates a new BrowserViewModel + @MainActor + private func createNewWindow(with url: URL) -> Bool { + guard let currentWindow = NSApp.keyWindow, + let windowController = currentWindow.windowController else { return false } + let context = Database.shared.viewContext + // create a new Tab DB object, and a new BrowserViewModel (which creates a new WKWebView) + // and pre-load the url into that + let newTab = NavigationViewModel.makeTab(context: context) + let newTabID = newTab.objectID + BrowserViewModel.getCached(tabID: newTabID).load(url: url) + + // store the tabID statically, so that the new window can pick it up + NavigationViewModel.tabIDToUseOnNewTab = newTabID + windowController.newWindowForTab(self) - // now reset the static url to nil, as the new BrowserViewModel already has it - BrowserViewModel.urlForNewTab = nil - guard let newWindow = NSApp.keyWindow, currentWindow != newWindow else { return false } + guard let newWindow = NSApp.keyWindow, currentWindow != newWindow else { + // rather impossible case, but rolling back everything from above + NavigationViewModel.tabIDToUseOnNewTab = nil + context.delete(newTab) + if context.hasChanges { + try? context.save() + } + return false + } currentWindow.addTabbedWindow(newWindow, ordered: .above) return true } @@ -347,7 +390,7 @@ final class BrowserViewModel: NSObject, ObservableObject, #if os(macOS) // detect cmd + click event if navigationAction.modifierFlags.contains(.command) { - if createNewTab(url: url) { + if createNewWindow(with: url) { return .cancel } } @@ -475,6 +518,7 @@ final class BrowserViewModel: NSObject, ObservableObject, // MARK: - WKUIDelegate #if os(macOS) + @MainActor func webView( _: WKWebView, createWebViewWith _: WKWebViewConfiguration, @@ -490,10 +534,11 @@ final class BrowserViewModel: NSObject, ObservableObject, return nil } - _ = createNewTab(url: newUrl) + _ = createNewWindow(with: newUrl) return nil } #else + @MainActor func webView( _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, @@ -508,7 +553,6 @@ final class BrowserViewModel: NSObject, ObservableObject, externalURL = newURL return nil } - NotificationCenter.openURL(newURL, inNewTab: true) return nil } #endif @@ -529,25 +573,29 @@ final class BrowserViewModel: NSObject, ObservableObject, webView.load(URLRequest(url: url)) return WebViewController(webView: webView) }, - actionProvider: { _ in + actionProvider: { [weak self] _ in + guard let self else { return UIMenu(children: []) } var actions = [UIAction]() // open url actions.append( UIAction(title: "common.dialog.button.open".localized, - image: UIImage(systemName: "doc.text")) { _ in - webView.load(URLRequest(url: url)) + image: UIImage(systemName: "doc.text")) { [weak self] _ in + self?.webView.load(URLRequest(url: url)) } ) actions.append( UIAction(title: "common.dialog.button.open_in_new_tab".localized, - image: UIImage(systemName: "doc.badge.plus")) { _ in - NotificationCenter.openURL(url, inNewTab: true) + image: UIImage(systemName: "doc.badge.plus")) { [weak self] _ in + guard let self else { return } + Task { @MainActor in + NotificationCenter.openURL(url, inNewTab: true) + } } ) // bookmark - let bookmarkAction: UIAction = { + let bookmarkAction: UIAction = { [weak self] in let context = Database.shared.viewContext let predicate = NSPredicate(format: "articleURL == %@", url as CVarArg) let request = Bookmark.fetchRequest(predicate: predicate) @@ -576,75 +624,6 @@ final class BrowserViewModel: NSObject, ObservableObject, } #endif - // MARK: - TabID management via NSWindow for macOS - -#if os(macOS) - private(set) var windowNumber: Int? - - // RESTORATION - func restoreByWindowNumber( - windowNumber currentNumber: Int, - urlToTabIdConverter: @MainActor @escaping (URL?) -> NSManagedObjectID - ) { - windowNumber = currentNumber - let windows = NSApplication.shared.windows - let tabURL: URL? - - guard let currentWindow = windowBy(number: currentNumber), - let index = windows.firstIndex(of: currentWindow) else { return } - - // find the url for this window in user defaults, by pure index - if 0 <= index, - index < windowURLs.count { - tabURL = windowURLs[index] - } else { - tabURL = nil - } - Task { - await MainActor.run { - let tabID = urlToTabIdConverter(tabURL) // if url is nil it will create a new tab - self.tabID = tabID - restoreBy(tabID: tabID) - } - } - } - - private func indexOf(windowNumber number: Int, in windows: [NSWindow]) -> Int? { - let windowNumbers = windows.map { $0.windowNumber } - guard windowNumbers.contains(number), - let index = windowNumbers.firstIndex(of: number) else { - return nil - } - return index - } - - // PERSISTENCE: - private func storeTabIDInCurrentWindow() { - guard let tabID, - let windowNumber, - let currentWindow = windowBy(number: windowNumber) else { - return - } - let url = tabID.uriRepresentation() - currentWindow.setAccessibilityURL(url) - } - - private func windowBy(number: Int) -> NSWindow? { - NSApplication.shared.windows.first { $0.windowNumber == number } - } -#endif - - private func createNewTabID() -> NSManagedObjectID { - if let tabID { return tabID } - let context = Database.shared.viewContext - let tab = Tab(context: context) - tab.created = Date() - tab.lastOpened = Date() - try? context.obtainPermanentIDs(for: [tab]) - try? context.save() - return tab.objectID - } - // MARK: - Bookmark func controller(_: NSFetchedResultsController, @@ -655,7 +634,7 @@ final class BrowserViewModel: NSObject, ObservableObject, @MainActor func createBookmark(url: URL? = nil) { guard let url = url ?? webView.url, - let zimFileID = UUID(uuidString: url.host ?? "") else { return } + let zimFileID = url.zimFileID else { return } let title = webView.title Task { guard let metaData = await ZimFileService.shared.getContentMetaData(url: url) else { return } diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift index 2c8aea0a9..c41ab3e37 100644 --- a/ViewModel/LaunchViewModel.swift +++ b/ViewModel/LaunchViewModel.swift @@ -91,8 +91,8 @@ final class NoCatalogLaunchViewModel: LaunchViewModelBase { // MARK: With Catalog Library final class CatalogLaunchViewModel: LaunchViewModelBase { - private var hasZIMFiles = CurrentValueSubject(false) - private var hasSeenCategoriesOnce = CurrentValueSubject(false) + private var hasZIMFiles = PassthroughSubject() + private var hasSeenCategoriesOnce = PassthroughSubject() convenience init(library: LibraryViewModel, browser: BrowserViewModel) { diff --git a/ViewModel/NavigationViewModel.swift b/ViewModel/NavigationViewModel.swift index da2fb6af3..322be8d28 100644 --- a/ViewModel/NavigationViewModel.swift +++ b/ViewModel/NavigationViewModel.swift @@ -18,15 +18,37 @@ import WebKit @MainActor final class NavigationViewModel: ObservableObject { + let uuid = UUID() // remained optional due to focusedSceneValue conformance @Published var currentItem: NavigationItem? = .loading #if os(macOS) var isTerminating: Bool = false + + /// Used to store temporarily the value of the tabID for a newly opened window + static var tabIDToUseOnNewTab: NSManagedObjectID? + + var currentTabId: NSManagedObjectID { + guard let currentTabIdValue else { + let newTabId: NSManagedObjectID + // when opening a new tab, use a preconfigured tabID + if let tabIDToUseOnNewTab = Self.tabIDToUseOnNewTab { + newTabId = tabIDToUseOnNewTab + Self.tabIDToUseOnNewTab = nil + } else { + newTabId = createTab() + } + currentTabIdValue = newTabId + return newTabId + } + return currentTabIdValue + } + + private var currentTabIdValue: NSManagedObjectID? #endif // MARK: - Tab Management - private func makeTab(context: NSManagedObjectContext) -> Tab { + static func makeTab(context: NSManagedObjectContext) -> Tab { let tab = Tab(context: context) tab.created = Date() tab.lastOpened = Date() @@ -39,7 +61,7 @@ final class NavigationViewModel: ObservableObject { let context = Database.shared.viewContext let fetchRequest = Tab.fetchRequest(sortDescriptors: [NSSortDescriptor(key: "lastOpened", ascending: false)]) fetchRequest.fetchLimit = 1 - let tab = (try? context.fetch(fetchRequest).first) ?? self.makeTab(context: context) + let tab = (try? context.fetch(fetchRequest).first) ?? Self.makeTab(context: context) Task { await MainActor.run { currentItem = NavigationItem.tab(objectID: tab.objectID) @@ -51,7 +73,7 @@ final class NavigationViewModel: ObservableObject { @discardableResult func createTab() -> NSManagedObjectID { let context = Database.shared.viewContext - let tab = self.makeTab(context: context) + let tab = Self.makeTab(context: context) #if !os(macOS) currentItem = NavigationItem.tab(objectID: tab.objectID) #endif @@ -83,9 +105,12 @@ final class NavigationViewModel: ObservableObject { return } let newlySelectedTab: Tab? - // select a closeBy tab if the currently selected tab is to be deleted if case let .tab(selectedTabID) = self.currentItem, selectedTabID == tabID { - newlySelectedTab = tabs.closeBy(toWhere: { $0.objectID == tabID }) ?? self.makeTab(context: context) + // select a closeBy tab if the currently selected tab is to be deleted + newlySelectedTab = tabs.closeBy(toWhere: { $0.objectID == tabID }) ?? Self.makeTab(context: context) + } else if tabs.count == 1 { + // we are deleting the last tab and the selection is somewhere else + newlySelectedTab = Self.makeTab(context: context) } else { newlySelectedTab = nil // the current selection should remain } @@ -113,7 +138,7 @@ final class NavigationViewModel: ObservableObject { tabs?.forEach { context.delete($0) } // create new tab - let newTab = self.makeTab(context: context) + let newTab = Self.makeTab(context: context) Task { await MainActor.run { self.currentItem = NavigationItem.tab(objectID: newTab.objectID) @@ -121,6 +146,22 @@ final class NavigationViewModel: ObservableObject { } } } + + #if os(macOS) + /// On closing a ZIM, this clears out the currentTabId if needed + /// Effectively recreating BrowserViewModel and the wkwebview + /// from scratch + /// - Parameter tabIds: the ones that should remain + func keepOnlyTabsBy(tabIds: Set) { + guard let currentId = currentTabIdValue, + !tabIds.contains(currentId) else { + return + } + // setting it to nil ensures a new tab (and webview) will be created + // on accessing the public currentTabId + currentTabIdValue = nil + } + #endif } extension Array { diff --git a/ViewModel/OrderedCache.swift b/ViewModel/OrderedCache.swift index 0fdc89b31..fcb930eea 100644 --- a/ViewModel/OrderedCache.swift +++ b/ViewModel/OrderedCache.swift @@ -46,6 +46,13 @@ final class OrderedCache { } } + func removeNotMatchingWith(keys: Set) -> [Value] { + let removableKeys = Set(dict.keys).subtracting(keys) + return removableKeys.compactMap { key in + dict.removeValue(forKey: key)?.value + } + } + func setValue(_ value: Value, forKey key: Key, dated: Date = Date.now) { dict[key] = ValueDated(value: value, date: dated) } diff --git a/Views/Bookmarks.swift b/Views/Bookmarks.swift index 65b650327..eb2df052f 100644 --- a/Views/Bookmarks.swift +++ b/Views/Bookmarks.swift @@ -16,6 +16,7 @@ import SwiftUI struct Bookmarks: View { + @EnvironmentObject private var navigation: NavigationViewModel @Environment(\.dismiss) private var dismiss @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.managedObjectContext) private var managedObjectContext diff --git a/Views/BuildingBlocks/ArticleCell.swift b/Views/BuildingBlocks/ArticleCell.swift index 1630ae3ef..211e4c2d5 100644 --- a/Views/BuildingBlocks/ArticleCell.swift +++ b/Views/BuildingBlocks/ArticleCell.swift @@ -17,7 +17,7 @@ import SwiftUI /// A rounded rect cell displaying preview of an article. struct ArticleCell: View { - @State var isHovering: Bool = false + @State private var isHovering: Bool = false let title: String let snippet: NSAttributedString? diff --git a/Views/BuildingBlocks/Favicon.swift b/Views/BuildingBlocks/Favicon.swift index 30778f945..277bd236f 100644 --- a/Views/BuildingBlocks/Favicon.swift +++ b/Views/BuildingBlocks/Favicon.swift @@ -16,7 +16,7 @@ import SwiftUI struct Favicon: View { - @State var imageData: Data? + @State private var imageData: Data? private let category: Category private let imageURL: URL? diff --git a/Views/BuildingBlocks/SheetContent.swift b/Views/BuildingBlocks/SheetContent.swift index 7ab299c8b..709b20fa1 100644 --- a/Views/BuildingBlocks/SheetContent.swift +++ b/Views/BuildingBlocks/SheetContent.swift @@ -28,7 +28,7 @@ struct SheetContent: View { #if os(macOS) content #elseif os(iOS) - NavigationView { + NavigationStack { content.toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { diff --git a/Views/BuildingBlocks/ZimFileCell.swift b/Views/BuildingBlocks/ZimFileCell.swift index b003c46eb..7c182b45e 100644 --- a/Views/BuildingBlocks/ZimFileCell.swift +++ b/Views/BuildingBlocks/ZimFileCell.swift @@ -18,7 +18,7 @@ import SwiftUI struct ZimFileCell: View { @ObservedObject var zimFile: ZimFile - @State var isHovering: Bool = false + @State private var isHovering: Bool = false let isLoading: Bool let prominent: Prominent diff --git a/Views/Buttons/BookmarkButton.swift b/Views/Buttons/BookmarkButton.swift index 81b5c0138..a5cba7151 100644 --- a/Views/Buttons/BookmarkButton.swift +++ b/Views/Buttons/BookmarkButton.swift @@ -69,7 +69,7 @@ struct BookmarkButton: View { } .help("bookmark_button.show.help".localized) .popover(isPresented: $isShowingPopOver) { - NavigationView { + NavigationStack { Bookmarks().navigationBarTitleDisplayMode(.inline).toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { diff --git a/Views/Buttons/OutlineButton.swift b/Views/Buttons/OutlineButton.swift index a7bc09d0d..909fafe86 100644 --- a/Views/Buttons/OutlineButton.swift +++ b/Views/Buttons/OutlineButton.swift @@ -43,7 +43,7 @@ struct OutlineButton: View { .disabled(browser.outlineItems.isEmpty) .help("outline_button.outline.help".localized) .popover(isPresented: $isShowingOutline) { - NavigationView { + NavigationStack { Group { if browser.outlineItemTree.isEmpty { Message(text: "outline_button.outline.empty.message".localized) diff --git a/Views/Buttons/TabsManagerButton.swift b/Views/Buttons/TabsManagerButton.swift index 2b2588cd1..1496ce943 100644 --- a/Views/Buttons/TabsManagerButton.swift +++ b/Views/Buttons/TabsManagerButton.swift @@ -19,10 +19,6 @@ import SwiftUI struct TabsManagerButton: View { @EnvironmentObject private var browser: BrowserViewModel @EnvironmentObject private var navigation: NavigationViewModel - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)], - predicate: ZimFile.openedPredicate - ) private var zimFiles: FetchedResults @State private var presentedSheet: PresentedSheet? private enum PresentedSheet: String, Identifiable { @@ -58,7 +54,7 @@ struct TabsManagerButton: View { .sheet(item: $presentedSheet) { presentedSheet in switch presentedSheet { case .tabsManager: - NavigationView { + NavigationStack { TabManager().toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { @@ -83,7 +79,7 @@ struct TabManager: View { ) private var tabs: FetchedResults var body: some View { - List(tabs) { tab in + List(tabs, id: \.self) { tab in Button { navigation.currentItem = NavigationItem.tab(objectID: tab.objectID) } label: { diff --git a/Views/Commands.swift b/Views/Commands.swift index 3b47d353e..82eb71d9e 100644 --- a/Views/Commands.swift +++ b/Views/Commands.swift @@ -92,7 +92,7 @@ struct SidebarNavigationCommands: View { @FocusedBinding(\.navigationItem) var navigationItem: NavigationItem?? var body: some View { - buildButtons([.reading, .bookmarks], modifiers: [.command]) + buildButtons([.bookmarks], modifiers: [.command]) if FeatureFlags.hasLibrary { Divider() buildButtons([.opened, .categories, .downloads, .new], modifiers: [.command, .control]) diff --git a/Views/Library/Library.swift b/Views/Library/Library.swift index dee410e05..78d0da67c 100644 --- a/Views/Library/Library.swift +++ b/Views/Library/Library.swift @@ -111,6 +111,7 @@ struct LibraryZimFileDetailSidePanel: ViewModifier { /// On iOS, converts the modified view to a NavigationLink that goes to the zim file detail. struct LibraryZimFileContext: ViewModifier { @EnvironmentObject private var viewModel: LibraryViewModel + @EnvironmentObject private var navigation: NavigationViewModel let zimFile: ZimFile let dismiss: (() -> Void)? // iOS only diff --git a/Views/Library/ZimFileDetail.swift b/Views/Library/ZimFileDetail.swift index 208026b77..ff8f9a1e9 100644 --- a/Views/Library/ZimFileDetail.swift +++ b/Views/Library/ZimFileDetail.swift @@ -24,6 +24,7 @@ import Defaults struct ZimFileDetail: View { @Default(.downloadUsingCellular) private var downloadUsingCellular @Environment(\.dismiss) var dismiss + @EnvironmentObject var navigation: NavigationViewModel @ObservedObject var zimFile: ZimFile @State private var isPresentingDeleteAlert = false @State private var isPresentingDownloadAlert = false diff --git a/Views/SearchResults.swift b/Views/SearchResults.swift index 533a9f021..62b6d3449 100644 --- a/Views/SearchResults.swift +++ b/Views/SearchResults.swift @@ -23,6 +23,7 @@ struct SearchResults: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.managedObjectContext) private var managedObjectContext @EnvironmentObject private var viewModel: SearchViewModel + @EnvironmentObject private var navigation: NavigationViewModel @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)], predicate: ZimFile.Predicate.isDownloaded, diff --git a/Views/Settings/About.swift b/Views/Settings/About.swift index 7c1f1de44..9eaea56db 100644 --- a/Views/Settings/About.swift +++ b/Views/Settings/About.swift @@ -158,6 +158,6 @@ private struct Dependency: Identifiable { #if os(macOS) TabView { About() } #elseif os(iOS) - NavigationView { About() } + NavigationStack { About() } #endif } diff --git a/Views/ViewModifiers/BookmarkContextMenu.swift b/Views/ViewModifiers/BookmarkContextMenu.swift index dddca7112..f16cb3b53 100644 --- a/Views/ViewModifiers/BookmarkContextMenu.swift +++ b/Views/ViewModifiers/BookmarkContextMenu.swift @@ -19,6 +19,7 @@ import SwiftUI struct BookmarkContextMenu: ViewModifier { @Environment(\.managedObjectContext) private var managedObjectContext + @EnvironmentObject private var navigation: NavigationViewModel let bookmark: Bookmark diff --git a/Views/ViewModifiers/FileImport.swift b/Views/ViewModifiers/FileImport.swift index ccb914af2..f0734f641 100644 --- a/Views/ViewModifiers/FileImport.swift +++ b/Views/ViewModifiers/FileImport.swift @@ -18,7 +18,8 @@ import UniformTypeIdentifiers /// Button that presents a file importer. /// Note 1: Does not work on iOS / iPadOS via commands. -/// Note 2: Does not allow multiple selection, because we want a new tab to be opened with main page when file is opened, +/// Note 2: Does not allow multiple selection, +/// because we want a new tab to be opened with main page when file is opened, /// and the multitab implementation on iOS / iPadOS does not support open multiple tabs with an url right now. struct OpenFileButton: View { @State private var isPresented: Bool = false @@ -49,6 +50,7 @@ struct OpenFileButton: View { } struct OpenFileHandler: ViewModifier { + @EnvironmentObject private var navigation: NavigationViewModel @State private var isAlertPresented = false @State private var activeAlert: ActiveAlert? @@ -91,7 +93,7 @@ struct OpenFileHandler: ViewModifier { NotificationCenter.openURL(url, inNewTab: true) #endif } - case .onBoarding: + case .welcomeScreen: for fileID in openedZimFileIDs { guard let url = await ZimFileService.shared.getMainPageURL(zimFileID: fileID) else { return } NotificationCenter.openURL(url) diff --git a/Views/WelcomeCatalog.swift b/Views/WelcomeCatalog.swift index 87e71ce52..d85c6d8cd 100644 --- a/Views/WelcomeCatalog.swift +++ b/Views/WelcomeCatalog.swift @@ -72,7 +72,7 @@ struct WelcomeCatalog: View { } private var openFileButton: some View { - OpenFileButton(context: .onBoarding) { + OpenFileButton(context: .welcomeScreen) { HStack { Spacer() Text("welcome.actions.open_file".localized)