diff --git a/App/SplitViewController.swift b/App/SplitViewController.swift index 2e442a1b..13989faf 100644 --- a/App/SplitViewController.swift +++ b/App/SplitViewController.swift @@ -178,8 +178,13 @@ final class SplitViewController: UISplitViewController { }() setViewController(UINavigationController(rootViewController: controller), for: .secondary) case .opened: - let controller = UIHostingController(rootView: ZimFilesOpened(dismiss: nil)) - setViewController(UINavigationController(rootViewController: controller), for: .secondary) + // workaround for programatic triggering ZimFileDetails + // on iPad full screen view + let navHelper = NavigationHelper() + let controller = UIHostingController(rootView: ZimFilesOpened(navigationHelper: navHelper)) + let navController = UINavigationController(rootViewController: controller) + navHelper.navigationController = navController + setViewController(navController, for: .secondary) case .categories: let controller = UIHostingController(rootView: ZimFilesCategories(dismiss: nil)) setViewController(UINavigationController(rootViewController: controller), for: .secondary) diff --git a/SwiftUI/Patches.swift b/SwiftUI/Patches.swift index f1d36a20..cd094da1 100644 --- a/SwiftUI/Patches.swift +++ b/SwiftUI/Patches.swift @@ -71,6 +71,7 @@ extension Notification.Name { static let alert = Notification.Name("alert") static let openFiles = Notification.Name("openFiles") static let openURL = Notification.Name("openURL") + static let selectFile = Notification.Name("selectFile") static let exportFileData = Notification.Name("exportFileData") static let saveContent = Notification.Name("saveContent") static let toggleSidebar = Notification.Name("toggleSidebar") @@ -110,6 +111,11 @@ extension NotificationCenter { userInfo: userInfo ) } + + @MainActor + static func selectFileBy(fileId: UUID) { + NotificationCenter.default.post(name: .selectFile, object: nil, userInfo: ["fileId": fileId]) + } static func openFiles(_ urls: [URL], context: OpenFileContext) { NotificationCenter.default.post(name: .openFiles, object: nil, userInfo: ["urls": urls, "context": context]) diff --git a/Views/Library/Library.swift b/Views/Library/Library.swift index 4d389b53..a62981f5 100644 --- a/Views/Library/Library.swift +++ b/Views/Library/Library.swift @@ -58,7 +58,7 @@ struct Library: View { .listStyle(.plain) .navigationTitle(MenuItem.categories.name) case .opened: - ZimFilesOpened(dismiss: dismiss) + ZimFilesOpenedNavStack(dismiss: dismiss) case .downloads: ZimFilesDownloads(dismiss: dismiss) .environment(\.managedObjectContext, Database.shared.viewContext) diff --git a/Views/Library/ZimFilesMultiOpened.swift b/Views/Library/ZimFilesMultiOpened.swift index f18a7741..cf1f13d2 100644 --- a/Views/Library/ZimFilesMultiOpened.swift +++ b/Views/Library/ZimFilesMultiOpened.swift @@ -27,6 +27,8 @@ struct ZimFilesMultiOpened: View { ) private var zimFiles: FetchedResults @State private var isFileImporterPresented = false @StateObject private var selection = MultiSelectedZimFilesViewModel() + private let selectFileById = NotificationCenter.default.publisher(for: .selectFile) + @State private var fileIdToOpen: UUID? var body: some View { VStack(spacing: 0) { @@ -57,9 +59,23 @@ struct ZimFilesMultiOpened: View { Message(text: LocalString.zim_file_opened_overlay_no_opened_message) } } + .onReceive(selectFileById, perform: { notification in + guard let fileId = notification.userInfo?["fileId"] as? UUID else { + fileIdToOpen = nil + return + } + fileIdToOpen = fileId + }) .onChange(of: zimFiles.count) { _ in - if let firstZimFile = zimFiles.first { - selection.singleSelect(zimFile: firstZimFile) + let selectedZimFile: ZimFile? + if let fileIdToOpen { + selectedZimFile = zimFiles.first { $0.fileID == fileIdToOpen } + self.fileIdToOpen = nil + } else { + selectedZimFile = zimFiles.first + } + if let selectedZimFile { + selection.singleSelect(zimFile: selectedZimFile) } else { selection.reset() } diff --git a/Views/Library/ZimFilesOpened.swift b/Views/Library/ZimFilesOpened.swift index 695bdd85..0d598fb8 100644 --- a/Views/Library/ZimFilesOpened.swift +++ b/Views/Library/ZimFilesOpened.swift @@ -17,8 +17,21 @@ import SwiftUI import UniformTypeIdentifiers #if os(iOS) + +final class NavigationHelper { + weak var navigationController: UINavigationController? + func push(@ViewBuilder _ view: () -> V) { + let hostingVC = UIHostingController(rootView: view()) + navigationController?.pushViewController(hostingVC, animated: true) + } +} + /// A grid of zim files that are opened, or was open but is now missing -/// iOS only +/// iOS only, only iPad splitView +/// the UINavigationController used in splitView doesn't work with +/// NavigationStack +/// therefore programatic selection of newly added file is with a +/// workaround struct ZimFilesOpened: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @FetchRequest( @@ -28,7 +41,9 @@ struct ZimFilesOpened: View { ) private var zimFiles: FetchedResults @State private var isFileImporterPresented = false @EnvironmentObject var selection: SelectedZimFileViewModel - let dismiss: (() -> Void)? // iOS only + let navigationHelper: NavigationHelper + private let selectFileById = NotificationCenter.default.publisher(for: .selectFile) + @State private var fileIdToOpen: UUID? var body: some View { LazyVGrid( @@ -37,17 +52,15 @@ struct ZimFilesOpened: View { spacing: 12 ) { ForEach(zimFiles) { zimFile in - LibraryZimFileContext( - content: { - ZimFileCell( - zimFile, - prominent: .name, - isSelected: selection.isSelected(zimFile) - ) - }, - zimFile: zimFile, - selection: selection, - dismiss: dismiss) + NavigationLink { + ZimFileDetail(zimFile: zimFile, dismissParent: nil) + } label: { + ZimFileCell( + zimFile, + prominent: .name, + isSelected: selection.isSelected(zimFile) + ) + } .accessibilityIdentifier(zimFile.name) } } .modifier(GridCommon(edges: .all)) @@ -58,13 +71,34 @@ struct ZimFilesOpened: View { Message(text: LocalString.zim_file_opened_overlay_no_opened_message) } } + .onReceive(selectFileById, perform: { notification in + guard let fileId = notification.userInfo?["fileId"] as? UUID else { + fileIdToOpen = nil + return + } + fileIdToOpen = fileId + }) .onChange(of: zimFiles.count) { _ in - if let firstZimFile = zimFiles.first { - selection.selectedZimFile = firstZimFile + let selectedZimFile: ZimFile? + if let fileIdToOpen { + selectedZimFile = zimFiles.first { $0.fileID == fileIdToOpen } + self.fileIdToOpen = nil + } else { + selectedZimFile = nil + } + if let selectedZimFile { + selection.selectedZimFile = selectedZimFile } else { selection.reset() } } + .onReceive(selection.$selectedZimFile, perform: { selectedZimFile in + if let selectedZimFile { + navigationHelper.push { + ZimFileDetail(zimFile: selectedZimFile, dismissParent: nil) + } + } + }) // not using OpenFileButton here, because it does not work on iOS/iPadOS 15 when this view is in a modal .fileImporter( isPresented: $isFileImporterPresented, diff --git a/Views/Library/ZimFilesOpenedNavStack.swift b/Views/Library/ZimFilesOpenedNavStack.swift new file mode 100644 index 00000000..fafe35c4 --- /dev/null +++ b/Views/Library/ZimFilesOpenedNavStack.swift @@ -0,0 +1,101 @@ +// 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/. + +#if os(iOS) + +import SwiftUI +import UniformTypeIdentifiers + +/// A grid of zim files that are opened, or was open but is now missing +/// iOS only +struct ZimFilesOpenedNavStack: View { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)], + predicate: ZimFile.Predicate.isDownloaded, + animation: .easeInOut + ) private var zimFiles: FetchedResults + @State private var isFileImporterPresented = false + @State private var navPath: [ZimFile] = [] + // opening the details of a freshly added zimFile + private let selectFileById = NotificationCenter.default.publisher(for: .selectFile) + @State private var fileIdToOpen: UUID? + + let dismiss: (() -> Void)? + + var body: some View { + NavigationStack(path: $navPath) { + LazyVGrid( + columns: ([GridItem(.adaptive(minimum: 250, maximum: 500), spacing: 12)]), + alignment: .leading, + spacing: 12 + ) { + ForEach(zimFiles) { zimFile in + NavigationLink(value: zimFile) { + ZimFileCell( + zimFile, + prominent: .name, + isSelected: navPath.contains(where: { $0.fileID == zimFile.fileID }) + ) + }.accessibilityIdentifier(zimFile.name) + } + } + .navigationDestination(for: ZimFile.self) { zimFile in + ZimFileDetail(zimFile: zimFile, dismissParent: dismiss) + } + } + .modifier(GridCommon(edges: .all)) + .modifier(ToolbarRoleBrowser()) + .navigationTitle(MenuItem.opened.name) + .overlay { + if zimFiles.isEmpty { + Message(text: LocalString.zim_file_opened_overlay_no_opened_message) + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + isFileImporterPresented = true + } label: { + Label(LocalString.zim_file_opened_toolbar_open_title, systemImage: "plus") + }.help(LocalString.zim_file_opened_toolbar_open_help) + } + } + // not using OpenFileButton here, because it does not work on iOS/iPadOS 15 when this view is in a modal + .fileImporter( + isPresented: $isFileImporterPresented, + allowedContentTypes: [UTType.zimFile], + allowsMultipleSelection: true + ) { result in + guard case let .success(urls) = result else { return } + NotificationCenter.openFiles(urls, context: .library) + } + .onReceive(selectFileById, perform: { notification in + guard let fileId = notification.userInfo?["fileId"] as? UUID else { + return + } + fileIdToOpen = fileId + }) + .onChange(of: zimFiles.count) { _ in + if let fileIdToOpen, + let selectedZimFile = zimFiles.first(where: { $0.fileID == fileIdToOpen }) { + self.fileIdToOpen = nil + navPath = [selectedZimFile] + } + } + } +} + +#endif diff --git a/Views/ViewModifiers/FileImport.swift b/Views/ViewModifiers/FileImport.swift index ced1b252..73a59000 100644 --- a/Views/ViewModifiers/FileImport.swift +++ b/Views/ViewModifiers/FileImport.swift @@ -81,6 +81,11 @@ struct OpenFileHandler: ViewModifier { // action for zim files that can be opened (e.g. open main page) if case .library = context { // don't need to open the main page + // but we should select it to show the details + // if there's only one ZIM file imported + if openedZimFileIDs.count == 1, let firstFileID = openedZimFileIDs.first { + NotificationCenter.selectFileBy(fileId: firstFileID) + } } else { for fileID in openedZimFileIDs { if let url = await ZimFileService.shared.getMainPageURL(zimFileID: fileID) {