mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-22 11:03:21 -04:00
Merge pull request #1199 from kiwix/1197-link-to-current-page
1197 link to current page
This commit is contained in:
commit
2fe66010e1
@ -18,7 +18,6 @@ import UserNotifications
|
||||
import Combine
|
||||
import Defaults
|
||||
import CoreKiwix
|
||||
import PassKit
|
||||
|
||||
#if os(macOS)
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@ -37,6 +36,7 @@ struct Kiwix: App {
|
||||
@State private var selectedAmount: SelectedAmount?
|
||||
@StateObject var formReset = FormReset()
|
||||
@FocusState private var isSearchFocused: Bool
|
||||
@FocusedValue(\.browserURL) var browserURL
|
||||
|
||||
init() {
|
||||
UNUserNotificationCenter.current().delegate = notificationCenterDelegate
|
||||
@ -78,6 +78,16 @@ struct Kiwix: App {
|
||||
SidebarNavigationCommands()
|
||||
Divider()
|
||||
}
|
||||
CommandGroup(after: .pasteboard) {
|
||||
Button(LocalString.library_zim_file_context_copy_url) {
|
||||
if let browserURL {
|
||||
CopyPasteMenu.copyToPasteBoard(url: browserURL)
|
||||
}
|
||||
}
|
||||
.disabled(browserURL == nil)
|
||||
.keyboardShortcut("c", modifiers: [.command, .shift])
|
||||
|
||||
}
|
||||
CommandGroup(after: .textEditing) {
|
||||
Button(LocalString.common_search) {
|
||||
isSearchFocused = true
|
||||
@ -169,231 +179,4 @@ struct Kiwix: App {
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
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))
|
||||
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
|
||||
|
247
App/RootView_macOS.swift
Normal file
247
App/RootView_macOS.swift
Normal file
@ -0,0 +1,247 @@
|
||||
// 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]
|
||||
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))
|
||||
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
|
@ -39,6 +39,7 @@
|
||||
"common.button.no" = "no";
|
||||
"common.button.print" = "Print";
|
||||
"common.button.share" = "Share";
|
||||
"common.button.share_as_pdf" = "Share as PDF";
|
||||
"common.search" = "Search";
|
||||
|
||||
"common.tab.manager.title" = "Tabs Manager";
|
||||
|
@ -23,6 +23,7 @@
|
||||
"common.button.no" = "It is a title in the summary table on macOS: we list the attributes of a ZIM file: does it contain pictures yes/no, does it contain Videos yes/no, Details? yes/no";
|
||||
"common.button.print" = "Accessibility label for button to print the currently loaded article";
|
||||
"common.button.share" = "Accessibility label for share button, to share the currently loaded article with an external app.";
|
||||
"common.button.share_as_pdf" = "Accesibility label for share button. to share the curretly loadded article with an external application in a PDF file format.";
|
||||
"common.search" = "The default placeholder text for searchbars, when the search input field is empty";
|
||||
"common.tab.manager.title" = "Accessibility label for button tab bar button that opens an overlay menu.";
|
||||
"common.tab.navigation.title" = "On iPad it is a title in the sidemenu grouping tabs related buttons";
|
||||
|
@ -77,12 +77,21 @@ struct BrowserTab: View {
|
||||
}
|
||||
#else
|
||||
if !Brand.hideShareButton {
|
||||
ExportButton(
|
||||
relativeToView: browser.webView,
|
||||
webViewURL: browser.webView.url,
|
||||
pageDataWithExtension: { [weak browser] in await browser?.pageDataWithExtension() },
|
||||
isButtonDisabled: browser.zimFileName.isEmpty
|
||||
)
|
||||
Menu {
|
||||
ExportButton(
|
||||
relativeToView: browser.webView,
|
||||
webViewURL: browser.webView.url,
|
||||
pageDataWithExtension: { [weak browser] in await browser?.pageDataWithExtension() },
|
||||
isButtonDisabled: browser.zimFileName.isEmpty,
|
||||
buttonLabel: LocalString.common_button_share_as_pdf
|
||||
)
|
||||
if let url = browser.webView.url {
|
||||
CopyPasteMenu(url: url)
|
||||
.keyboardShortcut("c", modifiers: [.command, .shift])
|
||||
}
|
||||
} label: {
|
||||
Label(LocalString.common_button_share, systemImage: "square.and.arrow.up")
|
||||
}.disabled(browser.webView.url == nil)
|
||||
}
|
||||
if !Brand.hidePrintButton {
|
||||
PrintButton(browserURLName: { [weak browser] in
|
||||
@ -112,6 +121,9 @@ struct BrowserTab: View {
|
||||
}
|
||||
.environmentObject(search)
|
||||
.focusedSceneValue(\.isBrowserURLSet, browser.url != nil)
|
||||
#if os(macOS)
|
||||
.focusedSceneValue(\.browserURL, browser.url)
|
||||
#endif
|
||||
.focusedSceneValue(\.canGoBack, browser.canGoBack)
|
||||
.focusedSceneValue(\.canGoForward, browser.canGoForward)
|
||||
.modifier(ExternalLinkHandler(externalURL: $browser.externalURL))
|
||||
|
@ -18,18 +18,24 @@ import UniformTypeIdentifiers
|
||||
|
||||
struct CopyPasteMenu: View {
|
||||
|
||||
let downloadURL: URL
|
||||
let url: URL
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
#if os(macOS)
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(downloadURL.absoluteString, forType: .string)
|
||||
Self.copyToPasteBoard(url: url)
|
||||
#elseif os(iOS)
|
||||
UIPasteboard.general.setValue(downloadURL.absoluteString, forPasteboardType: UTType.url.identifier)
|
||||
UIPasteboard.general.setValue(url.absoluteString, forPasteboardType: UTType.url.identifier)
|
||||
#endif
|
||||
} label: {
|
||||
Label(LocalString.library_zim_file_context_copy_url, systemImage: "doc.on.doc")
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
public static func copyToPasteBoard(url: URL) {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(url.absoluteString, forType: .string)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ struct ExportButton: View {
|
||||
let webViewURL: URL?
|
||||
let pageDataWithExtension: () async -> (Data, String?)?
|
||||
let isButtonDisabled: Bool
|
||||
|
||||
var buttonLabel: String = LocalString.common_button_share
|
||||
|
||||
/// - Returns: Returns the browser data, fileName and extension
|
||||
private func dataNameAndExtension() async -> FileExportData? {
|
||||
@ -48,12 +50,22 @@ struct ExportButton: View {
|
||||
NotificationCenter.exportFileData(exportData)
|
||||
#else
|
||||
guard let url = await tempFileURL() else { return }
|
||||
NSSharingServicePicker(items: [url]).show(relativeTo: .null, of: relativeToView, preferredEdge: .minY)
|
||||
NSSharingServicePicker(items: [url]).show(
|
||||
relativeTo: NSRect(
|
||||
origin: .zero,
|
||||
size: CGSize(
|
||||
width: 640,
|
||||
height: 54
|
||||
)
|
||||
),
|
||||
of: relativeToView,
|
||||
preferredEdge: .minY
|
||||
)
|
||||
#endif
|
||||
}
|
||||
} label: {
|
||||
Label {
|
||||
Text(LocalString.common_button_share)
|
||||
Text(buttonLabel)
|
||||
} icon: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
|
@ -25,6 +25,10 @@ struct IsBrowserURLSet: FocusedValueKey {
|
||||
typealias Value = Bool
|
||||
}
|
||||
|
||||
struct BrowserURL: FocusedValueKey {
|
||||
typealias Value = URL
|
||||
}
|
||||
|
||||
struct CanGoBackKey: FocusedValueKey {
|
||||
typealias Value = Bool
|
||||
}
|
||||
@ -38,6 +42,12 @@ struct NavigationItemKey: FocusedValueKey {
|
||||
}
|
||||
|
||||
extension FocusedValues {
|
||||
#if os(macOS)
|
||||
var browserURL: BrowserURL.Value? {
|
||||
get { self[BrowserURL.self] }
|
||||
set { self[BrowserURL.self] = newValue}
|
||||
}
|
||||
#endif
|
||||
var isBrowserURLSet: IsBrowserURLSet.Value? {
|
||||
get { self[IsBrowserURLSet.self] }
|
||||
set { self[IsBrowserURLSet.self] = newValue }
|
||||
|
@ -23,7 +23,7 @@ struct ZimFileContextMenu: View {
|
||||
Section { ArticleActions(zimFileID: zimFile.fileID) }
|
||||
}
|
||||
if let downloadURL = zimFile.downloadURL {
|
||||
Section { CopyPasteMenu(downloadURL: downloadURL) }
|
||||
Section { CopyPasteMenu(url: downloadURL) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
34
Views/ViewModifiers/CopyURLContext.swift
Normal file
34
Views/ViewModifiers/CopyURLContext.swift
Normal file
@ -0,0 +1,34 @@
|
||||
// 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
|
||||
|
||||
struct CopyURLContext: ViewModifier {
|
||||
|
||||
@State var url: URL?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if let url {
|
||||
content.contextMenu {
|
||||
CopyPasteMenu(url: url)
|
||||
}
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
Loading…
x
Reference in New Issue
Block a user