kiwix-apple/App/RootView_macOS.swift
Balazs Perlaki-Horvath 9fd8378de0 Remove debug
2025-07-07 00:13:01 +02:00

250 lines
11 KiB
Swift

// 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(macOS)
import SwiftUI
import PassKit
struct RootView: View {
@Environment(\.openWindow) var openWindow
@Environment(\.controlActiveState) var controlActiveState
@StateObject private var navigation = NavigationViewModel()
@State private var currentNavItem: MenuItem?
@StateObject private var windowTracker = WindowTracker()
@State private var paymentButtonLabel: PayWithApplePayButtonLabel?
var isSearchFocused: FocusState<Bool>.Binding
@StateObject private var selection = SelectedZimFileViewModel()
// Open file alerts
@State private var isOpenFileAlertPresented = false
@State private var openFileAlert: OpenFileAlert?
private let primaryItems: [MenuItem] = [.bookmarks]
private let libraryItems: [MenuItem] = [.opened, .categories, .downloads, .new, .hotspot]
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)
private let goBackPublisher = NotificationCenter.default.publisher(for: .goBack)
private let goForwardPublisher = NotificationCenter.default.publisher(for: .goForward)
/// Close other tabs then the ones received
private let keepOnlyTabs = NotificationCenter.default.publisher(for: .keepOnlyTabs)
// in essence it's the "zim://" value
private static let zimURL: String = "\(KiwixURLSchemeHandler.ZIMScheme)://"
var body: some View {
NavigationSplitView {
List(selection: $currentNavItem) {
ForEach(
[MenuItem.tab(objectID: navigation.currentTabId)] + primaryItems,
id: \.self
) { menuItem in
Label(menuItem.name, systemImage: menuItem.icon)
}
if FeatureFlags.hasLibrary {
Section(LocalString.app_macos_navigation_button_library) {
ForEach(libraryItems, id: \.self) { menuItem in
Label(menuItem.name, systemImage: menuItem.icon)
}
}
}
}
.frame(minWidth: 160)
.safeAreaInset(edge: .bottom) {
if paymentButtonLabel != nil && Brand.hideDonation != true {
SupportKiwixButton {
openWindow(id: "donation")
}
}
}
} detail: {
switch navigation.currentItem {
case .loading:
LoadingDataView()
case .tab(let tabID):
BrowserTab(tabID: tabID)
.modifier(SearchFocused(isSearchFocused: isSearchFocused))
case .bookmarks:
Bookmarks()
.modifier(SearchFocused(isSearchFocused: isSearchFocused))
case .opened:
ZimFilesMultiOpened()
case .categories:
DetailSidePanel(content: { ZimFilesCategories(dismiss: nil) })
.modifier(SearchFocused(isSearchFocused: isSearchFocused))
case .downloads:
DetailSidePanel(content: { ZimFilesDownloads(dismiss: nil) })
case .new:
DetailSidePanel(content: { ZimFilesNew(dismiss: nil) })
.modifier(SearchFocused(isSearchFocused: isSearchFocused))
case .hotspot:
HotspotZimFilesSelection()
default:
EmptyView()
}
}
.frame(minWidth: 650, minHeight: 500)
.focusedSceneValue(\.navigationItem, $navigation.currentItem)
.modifier(AlertHandler())
.modifier(OpenFileHandler())
.modifier(SaveContentHandler())
.environmentObject(navigation)
.onChange(of: currentNavItem) { newValue in
navigation.currentItem = newValue?.navigationItem
}
.onChange(of: navigation.currentItem) { newValue in
guard let newValue else { return }
let navItem = MenuItem(from: newValue)
if currentNavItem != navItem {
currentNavItem = navItem
}
}
.onOpenURL { url in
/// if the app was just started via URL or file (wasn't open before)
/// we want to load the content in the first window
/// otherwise in a new tab (but within the currently active window)
let isAppStart = NSApplication.shared.windows.count == 1
if url.isFileURL {
// from opening an external file
let browser = BrowserViewModel.getCached(tabID: navigation.currentTabId)
if isAppStart {
browser.forceLoadingState()
}
Task { // open the ZIM file
if let metadata = await LibraryOperations.open(url: url),
let mainPageURL = await ZimFileService.shared.getMainPageURL(zimFileID: metadata.fileID) {
if isAppStart {
browser.load(url: mainPageURL)
} else {
browser.createNewWindow(with: mainPageURL)
}
} else {
await browser.clear()
isOpenFileAlertPresented = true
openFileAlert = .unableToOpen(filenames: [url.lastPathComponent])
}
}
} else if url.isZIMURL {
// from deeplinks
if isAppStart {
let browser = BrowserViewModel.getCached(tabID: navigation.currentTabId)
browser.load(url: url)
} else {
Task { @MainActor in
// we want to open the deeplink a new tab (in the currently active window)
// at this point though, the latest tab is active, that received the deeplink handling
// therefore we do need to wait 1 UI cycle, to finish the original
// deeplink handling (hence the Task { @MainActor solution)
// This way we can activate the newly opened tab with the deeplink content in it
let browser = BrowserViewModel.getCached(tabID: navigation.currentTabId)
browser.createNewWindow(with: url)
}
}
}
}
.alert(LocalString.file_import_alert_no_open_title,
isPresented: $isOpenFileAlertPresented, presenting: openFileAlert) { _ in
} message: { alert in
switch alert {
case .unableToOpen(let filenames):
let name = ListFormatter.localizedString(byJoining: filenames)
Text(LocalString.file_import_alert_no_open_message(withArgs: name))
}
}
.onReceive(openURL) { notification in
guard let url = notification.userInfo?["url"] as? URL else {
return
}
guard controlActiveState == .key else { return }
let tabID = navigation.currentTabId
currentNavItem = .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
guard windowTracker.current == publisher.object as? NSWindow else {
// when exiting full screen video, we get the same notification
// but that's not comming from our window
return
}
windowTracker.current = nil // remove the reference to this window, see guard above
guard !navigation.isTerminating else {
// tab closed by app termination
return
}
let tabID = navigation.currentTabId
let browser = BrowserViewModel.getCached(tabID: tabID)
// tab closed by user
browser.pauseVideoWhenNotInPIP()
navigation.deleteTab(tabID: tabID)
}
.onReceive(keepOnlyTabs) {notification in
guard let tabsToKeep = notification.userInfo?["tabIds"] as? Set<NSManagedObjectID> else {
return
}
navigation.keepOnlyTabsBy(tabIds: tabsToKeep)
}
.onReceive(appTerminates) { _ in
// CMD+Q -> Quit Kiwix, this also closes the last window
navigation.isTerminating = true
}.onReceive(goForwardPublisher) { _ in
guard case .tab(let tabID) = navigation.currentItem else {
return
}
BrowserViewModel.getCached(tabID: tabID).webView.goForward()
}.onReceive(goBackPublisher) { [weak navigation] _ in
guard case .tab(let tabID) = navigation?.currentItem else {
return
}
BrowserViewModel.getCached(tabID: tabID).webView.goBack()
}.task {
switch AppType.current {
case .kiwix:
await LibraryOperations.reopen()
currentNavItem = .tab(objectID: navigation.currentTabId)
LibraryOperations.scanDirectory(URL.documentDirectory)
LibraryOperations.applyFileBackupSetting()
DownloadService.shared.restartHeartbeatIfNeeded()
case let .custom(zimFileURL):
await LibraryOperations.open(url: zimFileURL)
ZimMigration.forCustomApps()
currentNavItem = .tab(objectID: navigation.currentTabId)
}
// MARK: - payment button init
if Brand.hideDonation == false {
paymentButtonLabel = await Payment.paymentButtonTypeAsync()
}
// MARK: - migrations
if !ProcessInfo.processInfo.arguments.contains("testing") {
_ = MigrationService().migrateAll()
}
}
// special hook to trigger the zim file search in the nav bar, when a web view is opened
// and the cmd+f is triggering the search in page
.onReceive(NotificationCenter.default.publisher(for: .zimSearch)) { _ in
isSearchFocused.wrappedValue = true
}
.withHostingWindow { [weak windowTracker] hostWindow in
windowTracker?.current = hostWindow
}
.handlesExternalEvents(
preferring: Set([Self.zimURL]),
allowing: Set([Self.zimURL, "file:///"])
)
}
}
#endif