Open ZimFileDetails when importing a single file

This commit is contained in:
Balazs Perlaki-Horvath 2025-07-27 22:19:38 +02:00
parent 3e0a31c467
commit 4b82c64589
7 changed files with 189 additions and 20 deletions

View File

@ -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, dismiss: nil))
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)

View File

@ -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])

View File

@ -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)

View File

@ -27,6 +27,8 @@ struct ZimFilesMultiOpened: View {
) private var zimFiles: FetchedResults<ZimFile>
@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()
}

View File

@ -17,8 +17,21 @@ import SwiftUI
import UniformTypeIdentifiers
#if os(iOS)
final class NavigationHelper {
weak var navigationController: UINavigationController?
func push<V: View>(@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,11 @@ struct ZimFilesOpened: View {
) private var zimFiles: FetchedResults<ZimFile>
@State private var isFileImporterPresented = false
@EnvironmentObject var selection: SelectedZimFileViewModel
let dismiss: (() -> Void)? // iOS only
let navigationHelper: NavigationHelper
let dismiss: (() -> Void)?
private let selectFileById = NotificationCenter.default.publisher(for: .selectFile)
@State private var fileIdToOpen: UUID?
var body: some View {
LazyVGrid(
@ -37,17 +54,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: dismiss)
} label: {
ZimFileCell(
zimFile,
prominent: .name,
isSelected: selection.isSelected(zimFile)
)
} .accessibilityIdentifier(zimFile.name)
}
}
.modifier(GridCommon(edges: .all))
@ -58,13 +73,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: dismiss)
}
}
})
// not using OpenFileButton here, because it does not work on iOS/iPadOS 15 when this view is in a modal
.fileImporter(
isPresented: $isFileImporterPresented,

View File

@ -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<ZimFile>
@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

View File

@ -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) {