mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-14 22:56:58 -04:00
MacOS window restoration fixes
This commit is contained in:
parent
9c7779ac24
commit
b7be0b44b2
@ -8,14 +8,15 @@
|
||||
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import Combine
|
||||
import Defaults
|
||||
|
||||
#if os(macOS)
|
||||
@main
|
||||
struct Kiwix: App {
|
||||
@StateObject private var libraryRefreshViewModel = LibraryViewModel()
|
||||
|
||||
private let notificationCenterDelegate = NotificationCenterDelegate()
|
||||
|
||||
|
||||
init() {
|
||||
UNUserNotificationCenter.current().delegate = notificationCenterDelegate
|
||||
LibraryOperations.reopen()
|
||||
@ -63,7 +64,7 @@ struct Kiwix: App {
|
||||
.environmentObject(libraryRefreshViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
|
||||
/// Handling file download complete notification
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
@ -86,7 +87,8 @@ struct RootView: View {
|
||||
private let primaryItems: [NavigationItem] = [.reading, .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)
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List(selection: $navigation.currentItem) {
|
||||
@ -111,6 +113,12 @@ struct RootView: View {
|
||||
switch navigation.currentItem {
|
||||
case .reading:
|
||||
BrowserTab().environmentObject(browser)
|
||||
.withHostingWindow { window in
|
||||
if let windowNumber = window?.windowNumber {
|
||||
browser.restoreByWindowNumber(windowNumber: windowNumber,
|
||||
urlToTabIdConverter: navigation.tabIDFor(url:))
|
||||
}
|
||||
}
|
||||
case .bookmarks:
|
||||
Bookmarks()
|
||||
case .opened:
|
||||
@ -139,9 +147,35 @@ struct RootView: View {
|
||||
}
|
||||
.onReceive(openURL) { notification in
|
||||
guard controlActiveState == .key, let url = notification.userInfo?["url"] as? URL else { return }
|
||||
browser.load(url: url)
|
||||
navigation.currentItem = .reading
|
||||
browser.load(url: url)
|
||||
}
|
||||
.onReceive(appTerminates) { _ in
|
||||
browser.persistAllTabIdsFromWindows()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: helpers to capture the window
|
||||
|
||||
extension View {
|
||||
func withHostingWindow(_ callback: @escaping (NSWindow?) -> Void) -> some View {
|
||||
self.background(HostingWindowFinder(callback: callback))
|
||||
}
|
||||
}
|
||||
|
||||
struct HostingWindowFinder: NSViewRepresentable {
|
||||
typealias NSViewType = NSView
|
||||
var callback: (NSWindow?) -> ()
|
||||
func makeNSView(context: Context) -> NSView {
|
||||
let view = NSView()
|
||||
DispatchQueue.main.async { [weak view] in
|
||||
self.callback(view?.window)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSView, context: Context) {}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -20,37 +20,41 @@ class KiwixURLSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
return
|
||||
}
|
||||
|
||||
objCTryBlock {
|
||||
/// Skipping handling for HTTP 206 Partial Content
|
||||
/// For video playback, WebKit makes a large amount of requests with small byte range (e.g. 8 bytes) to retrieve content of the video.
|
||||
/// As a result of the large volume of small requests, CPU usage will be very high, which can result in app or webpage frozen.
|
||||
/// To mitigate, opting for the less "broken" behavior of ignoring Range header until WebKit behavior is changed.
|
||||
//if let range = urlSchemeTask.request.allHTTPHeaderFields?["Range"] as? String {
|
||||
// let parts = range.components(separatedBy: ["=", "-"])
|
||||
// guard parts.count >= 2, let start = UInt(parts[1]) else {
|
||||
// self.sendHTTP400Response(urlSchemeTask, url: url)
|
||||
// return
|
||||
// }
|
||||
// let end = parts.count == 3 ? UInt(parts[2]) ?? 0 : 0
|
||||
// guard let content = ZimFileService.shared.getURLContent(
|
||||
// url: url, start: start, end: end
|
||||
// ) else {
|
||||
// self.sendHTTP404Response(urlSchemeTask, url: url)
|
||||
// return
|
||||
// }
|
||||
// self.sendHTTP206Response(urlSchemeTask, url: url, content: content)
|
||||
//} else {
|
||||
// guard let content = ZimFileService.shared.getURLContent(url: url) else {
|
||||
// self.sendHTTP404Response(urlSchemeTask, url: url)
|
||||
// return
|
||||
// }
|
||||
// self.sendHTTP200Response(urlSchemeTask, url: url, content: content)
|
||||
//}
|
||||
guard let content = ZimFileService.shared.getURLContent(url: url) else {
|
||||
self.sendHTTP404Response(urlSchemeTask, url: url)
|
||||
return
|
||||
do {
|
||||
objCTryBlock {
|
||||
/// Skipping handling for HTTP 206 Partial Content
|
||||
/// For video playback, WebKit makes a large amount of requests with small byte range (e.g. 8 bytes) to retrieve content of the video.
|
||||
/// As a result of the large volume of small requests, CPU usage will be very high, which can result in app or webpage frozen.
|
||||
/// To mitigate, opting for the less "broken" behavior of ignoring Range header until WebKit behavior is changed.
|
||||
//if let range = urlSchemeTask.request.allHTTPHeaderFields?["Range"] as? String {
|
||||
// let parts = range.components(separatedBy: ["=", "-"])
|
||||
// guard parts.count >= 2, let start = UInt(parts[1]) else {
|
||||
// self.sendHTTP400Response(urlSchemeTask, url: url)
|
||||
// return
|
||||
// }
|
||||
// let end = parts.count == 3 ? UInt(parts[2]) ?? 0 : 0
|
||||
// guard let content = ZimFileService.shared.getURLContent(
|
||||
// url: url, start: start, end: end
|
||||
// ) else {
|
||||
// self.sendHTTP404Response(urlSchemeTask, url: url)
|
||||
// return
|
||||
// }
|
||||
// self.sendHTTP206Response(urlSchemeTask, url: url, content: content)
|
||||
//} else {
|
||||
// guard let content = ZimFileService.shared.getURLContent(url: url) else {
|
||||
// self.sendHTTP404Response(urlSchemeTask, url: url)
|
||||
// return
|
||||
// }
|
||||
// self.sendHTTP200Response(urlSchemeTask, url: url, content: content)
|
||||
//}
|
||||
guard let content = ZimFileService.shared.getURLContent(url: url) else {
|
||||
self.sendHTTP404Response(urlSchemeTask, url: url)
|
||||
return
|
||||
}
|
||||
self.sendHTTP200Response(urlSchemeTask, url: url, content: content)
|
||||
}
|
||||
self.sendHTTP200Response(urlSchemeTask, url: url, content: content)
|
||||
} catch(let error) {
|
||||
debugPrint(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,11 @@ extension Defaults.Keys {
|
||||
|
||||
static let downloadUsingCellular = Key<Bool>("downloadUsingCellular", default: false)
|
||||
static let backupDocumentDirectory = Key<Bool>("backupDocumentDirectory", default: false)
|
||||
|
||||
#if os(macOS)
|
||||
// window management:
|
||||
static let windowURLs = Key<[URL]>("windowURLs", default: [])
|
||||
#endif
|
||||
}
|
||||
|
||||
extension Defaults.Serializable where Self: Codable {
|
||||
|
@ -10,6 +10,7 @@ import WebKit
|
||||
|
||||
final class BrowserNavDelegate: NSObject, WKNavigationDelegate {
|
||||
@Published private(set) var externalURL: URL?
|
||||
@Published private(set) var didLoadContent: Bool?
|
||||
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
@ -57,6 +58,8 @@ final class BrowserNavDelegate: NSObject, WKNavigationDelegate {
|
||||
webView.evaluateJavaScript("expandAllDetailTags(); getOutlineItems();")
|
||||
#if os(iOS)
|
||||
webView.adjustTextSize()
|
||||
#else
|
||||
didLoadContent = true
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import Combine
|
||||
import CoreData
|
||||
import CoreLocation
|
||||
import WebKit
|
||||
import Defaults
|
||||
|
||||
import OrderedCollections
|
||||
|
||||
@ -46,7 +47,20 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
@Published private(set) var url: URL?
|
||||
@Published var externalURL: URL?
|
||||
|
||||
let tabID: NSManagedObjectID?
|
||||
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
|
||||
private var canGoBackObserver: NSKeyValueObservation?
|
||||
private var canGoForwardObserver: NSKeyValueObservation?
|
||||
@ -77,6 +91,12 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
navDelegate.$externalURL.assign(to: \.externalURL, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
navDelegate.$didLoadContent.sink { [weak self] didLoad in
|
||||
if didLoad == true {
|
||||
self?.persistState()
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
|
||||
uiDelegate.$externalURL.assign(to: \.externalURL, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
@ -90,14 +110,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
|
||||
// restore webview state, and set url before observer call back
|
||||
// note: optionality of url determines what to show in a tab, so it should be set before tab is on screen
|
||||
if let tabID, let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab {
|
||||
webView.interactionState = tab.interactionState
|
||||
url = webView.url
|
||||
}
|
||||
if let urlForNewTab = Self.urlForNewTab {
|
||||
url = urlForNewTab
|
||||
load(url: urlForNewTab)
|
||||
}
|
||||
|
||||
|
||||
// configure web view
|
||||
webView.allowsBackForwardNavigationGestures = true
|
||||
@ -107,6 +120,14 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
webView.navigationDelegate = navDelegate
|
||||
webView.uiDelegate = uiDelegate
|
||||
|
||||
if let tabID {
|
||||
restoreBy(tabID: tabID)
|
||||
}
|
||||
if let urlForNewTab = Self.urlForNewTab {
|
||||
url = urlForNewTab
|
||||
load(url: urlForNewTab)
|
||||
}
|
||||
|
||||
// get outline items if something is already loaded
|
||||
if webView.url != nil {
|
||||
webView.evaluateJavaScript("getOutlineItems();")
|
||||
@ -131,20 +152,28 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
return try? Database.viewContext.fetch(ZimFile.fetchRequest(fileID: zimFileID)).first
|
||||
}()
|
||||
|
||||
// update view model
|
||||
self?.articleTitle = title
|
||||
self?.zimFileName = zimFile?.name ?? ""
|
||||
self?.url = url
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
// update view model
|
||||
strongSelf.articleTitle = title
|
||||
strongSelf.zimFileName = zimFile?.name ?? ""
|
||||
strongSelf.url = url
|
||||
|
||||
let currentTabID: NSManagedObjectID
|
||||
if let tabID = strongSelf.tabID {
|
||||
currentTabID = tabID
|
||||
} else {
|
||||
currentTabID = strongSelf.createNewTabID()
|
||||
strongSelf.tabID = currentTabID
|
||||
}
|
||||
// update tab data
|
||||
if let tabID = self?.tabID,
|
||||
let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab {
|
||||
if let tab = try? Database.viewContext.existingObject(with: currentTabID) as? Tab {
|
||||
tab.title = title
|
||||
tab.zimFile = zimFile
|
||||
}
|
||||
|
||||
// setup bookmark fetched results controller
|
||||
self?.bookmarkFetchedResultsController = NSFetchedResultsController(
|
||||
strongSelf.bookmarkFetchedResultsController = NSFetchedResultsController(
|
||||
fetchRequest: Bookmark.fetchRequest(predicate: {
|
||||
return NSPredicate(format: "articleURL == %@", url as CVarArg)
|
||||
}()),
|
||||
@ -152,8 +181,8 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
self?.bookmarkFetchedResultsController?.delegate = self
|
||||
try? self?.bookmarkFetchedResultsController?.performFetch()
|
||||
strongSelf.bookmarkFetchedResultsController?.delegate = self
|
||||
try? strongSelf.bookmarkFetchedResultsController?.performFetch()
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,7 +192,10 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
}
|
||||
|
||||
func persistState() {
|
||||
guard let tabID, let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab else { return }
|
||||
guard let tabID,
|
||||
let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab else {
|
||||
return
|
||||
}
|
||||
tab.interactionState = webView.interactionState as? Data
|
||||
try? Database.viewContext.save()
|
||||
}
|
||||
@ -187,6 +219,88 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
load(url: url)
|
||||
}
|
||||
|
||||
private func restoreBy(tabID: NSManagedObjectID) {
|
||||
if let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab {
|
||||
webView.interactionState = tab.interactionState
|
||||
url = webView.url
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TabID management via NSWindow for macOS
|
||||
|
||||
#if os(macOS)
|
||||
private (set) var windowNumber: Int?
|
||||
|
||||
// RESTORATION
|
||||
func restoreByWindowNumber(
|
||||
windowNumber currentNumber: Int,
|
||||
urlToTabIdConverter: @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
|
||||
}
|
||||
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:
|
||||
func persistAllTabIdsFromWindows() {
|
||||
let urls = NSApplication.shared.windows.compactMap { window in
|
||||
window.accessibilityURL()
|
||||
}
|
||||
UserDefaults.standard[.windowURLs] = urls
|
||||
}
|
||||
|
||||
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.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<NSFetchRequestResult>,
|
||||
|
@ -12,7 +12,8 @@ import WebKit
|
||||
@MainActor
|
||||
class NavigationViewModel: ObservableObject {
|
||||
@Published var currentItem: NavigationItem?
|
||||
|
||||
@Published var readingURL: URL?
|
||||
|
||||
init() {
|
||||
#if os(macOS)
|
||||
currentItem = .reading
|
||||
@ -46,10 +47,23 @@ class NavigationViewModel: ObservableObject {
|
||||
let tab = self.makeTab(context: context)
|
||||
try? context.obtainPermanentIDs(for: [tab])
|
||||
try? context.save()
|
||||
#if !os(macOS) //TODO: maybe we don't need this for iOS either
|
||||
currentItem = NavigationItem.tab(objectID: tab.objectID)
|
||||
#endif
|
||||
return tab.objectID
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
func tabIDFor(url: URL?) -> NSManagedObjectID {
|
||||
guard let url else {
|
||||
return createTab()
|
||||
}
|
||||
guard let tabID = Database.viewContext.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: url) else {
|
||||
return createTab()
|
||||
}
|
||||
return tabID
|
||||
}
|
||||
|
||||
/// Delete a single tab, and select another tab
|
||||
/// - Parameter tabID: ID of the tab to delete
|
||||
func deleteTab(tabID: NSManagedObjectID) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user