V3.0 bug fixes (#472)

* fix: bookmarks not removed when unlinking zim file

* fix: zim file not deleted when deleting zim file

* fix: pop view on unlink / delete

* build number

* webview loading concurrency issue

* build number

* fix: search text & results cleared when hiding keyboard

* fix: some zim files (e.g. ifixit) are missing category

* disable random & main article button when no opened zim file

* iPadOS: multi window

* build number

* macOS: navigation item keyboard shortcut

* build number

* another attempt at fixing crashes when attempting to sending data back at WKURLSchemeTask

* build number

* Revert "another attempt at fixing crashes when attempting to sending data back at WKURLSchemeTask"

This reverts commit cf698483727268a1b1467cb6222b7f038d19d6df.

* ignore NSExceptions

* resolve compile warning

* update to xcode recommended project settings

* build number

* more deterministic sorting

* remove duplicated search texts

* build number

* build number
This commit is contained in:
ChrisLi 2022-11-13 11:02:41 -05:00 committed by GitHub
parent 70a571277d
commit baeb36eebb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 137 additions and 96 deletions

View File

@ -310,7 +310,7 @@
97F425C227151A0D00D0F738 /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97F425C127151A0D00D0F738 /* QuickLook.framework */; };
97F425C527151A0D00D0F738 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F425C427151A0D00D0F738 /* PreviewViewController.swift */; };
97F425CA27151A0D00D0F738 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97F425C827151A0D00D0F738 /* MainInterface.storyboard */; };
97F425CE27151A0D00D0F738 /* QuickLookPreview.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 97F425C027151A0D00D0F738 /* QuickLookPreview.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
97F425CE27151A0D00D0F738 /* QuickLookPreview.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 97F425C027151A0D00D0F738 /* QuickLookPreview.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
97F425D327151E9C00D0F738 /* ZimFileService.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9779A5A92456793500F6F6FF /* ZimFileService.mm */; };
97F425D427151E9C00D0F738 /* ZimFileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9779A5AA2456793500F6F6FF /* ZimFileService.swift */; };
97F425D527151EF800D0F738 /* ZimFileMetaData.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9779A5AC2456793600F6F6FF /* ZimFileMetaData.mm */; };
@ -374,15 +374,15 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
97C57602202CB0E900E37502 /* Embed App Extensions */ = {
97C57602202CB0E900E37502 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
97F425CE27151A0D00D0F738 /* QuickLookPreview.appex in Embed App Extensions */,
97F425CE27151A0D00D0F738 /* QuickLookPreview.appex in Embed Foundation Extensions */,
);
name = "Embed App Extensions";
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
@ -1206,7 +1206,7 @@
97A36C261F8C21210079B452 /* Sources */,
97A36C341F8C21210079B452 /* Frameworks */,
97A36C431F8C21210079B452 /* Resources */,
97C57602202CB0E900E37502 /* Embed App Extensions */,
97C57602202CB0E900E37502 /* Embed Foundation Extensions */,
977BD604276EA929004D2C32 /* Embed Frameworks */,
);
buildRules = (
@ -1287,7 +1287,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1330;
LastUpgradeCheck = 1310;
LastUpgradeCheck = 1410;
ORGANIZATIONNAME = "Chris Li";
TargetAttributes = {
970EC3F023BE8E20008DCA27 = {
@ -1900,7 +1900,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 99;
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_ASSET_PATHS = "\"SwiftUI/Support\"";
DEVELOPMENT_TEAM = L7HWM3SP3L;
ENABLE_PREVIEWS = YES;
@ -1918,7 +1918,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0;
MARKETING_VERSION = 3.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = self.Kiwix;
@ -1940,7 +1940,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 99;
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_ASSET_PATHS = "\"SwiftUI/Support\"";
DEVELOPMENT_TEAM = L7HWM3SP3L;
ENABLE_PREVIEWS = YES;
@ -1958,7 +1958,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0;
MARKETING_VERSION = 3.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = self.Kiwix;
PRODUCT_NAME = Kiwix;
@ -1987,7 +1987,7 @@
"$(LD_RUNPATH_SEARCH_PATHS_$(IS_MACCATALYST))",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.13;
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
MARKETING_VERSION = 2.1.2;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = org.kiwix.KiwixMacOS;
@ -2019,7 +2019,7 @@
"$(LD_RUNPATH_SEARCH_PATHS_$(IS_MACCATALYST))",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.13;
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
MARKETING_VERSION = 2.1.2;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = org.kiwix.KiwixMacOS;
@ -2080,8 +2080,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_SWIFT_FLAGS = "";
@ -2135,8 +2135,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
@ -2287,7 +2287,8 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 99;
CURRENT_PROJECT_VERSION = 107;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"SwiftUI/Support\"";
DEVELOPMENT_TEAM = L7HWM3SP3L;
ENABLE_HARDENED_RUNTIME = YES;
@ -2301,7 +2302,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 2.0;
MARKETING_VERSION = 3.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = self.Kiwix;
@ -2327,7 +2328,8 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 99;
CURRENT_PROJECT_VERSION = 107;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"SwiftUI/Support\"";
DEVELOPMENT_TEAM = L7HWM3SP3L;
ENABLE_HARDENED_RUNTIME = YES;
@ -2341,7 +2343,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 2.0;
MARKETING_VERSION = 3.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = self.Kiwix;
PRODUCT_NAME = Kiwix;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1330"
LastUpgradeVersion = "1410"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1310"
LastUpgradeVersion = "1410"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1310"
LastUpgradeVersion = "1410"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1310"
LastUpgradeVersion = "1410"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1310"
LastUpgradeVersion = "1410"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1330"
LastUpgradeVersion = "1410"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -138,7 +138,7 @@ extension Realm {
guard let zimFileData = existingZimFiles.popLast() else { continue }
zimFile.articleCount = zimFileData.articleCount
zimFile.category = zimFileData.category
zimFile.category = (Category(rawValue: zimFileData.category) ?? .other).rawValue
zimFile.created = zimFileData.created
zimFile.downloadURL = zimFileData.downloadURL
zimFile.faviconData = zimFileData.faviconData

View File

@ -7,3 +7,14 @@
#import "ZimFileMetaData.h"
#import "SearchOperation.h"
#import "SearchResult.h"
NS_INLINE NSException * _Nullable objCTryBlock(void(^_Nonnull tryBlock)(void)) {
@try {
tryBlock();
return nil;
}
@catch (NSException *exception) {
return exception;
}
}

View File

@ -10,59 +10,54 @@ import os
import WebKit
class KiwixURLSchemeHandler: NSObject, WKURLSchemeHandler {
private var activeRequests = Set<URLRequest>()
private let activeRequestsSemaphore = DispatchSemaphore(value: 1)
private let dataFetchingSemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount)
private var urls = Set<URL>()
private var queue = DispatchQueue(label: "org.kiwix.webContent", qos: .userInitiated)
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
// unpack zimFileID and content path from the url
guard let url = urlSchemeTask.request.url,
url.isKiwixURL else {
queue.async {
// unpack zimFileID and content path from the url
guard let url = urlSchemeTask.request.url, url.isKiwixURL else {
urlSchemeTask.didFailWithError(URLError(.unsupportedURL))
return
}
// remember this active url scheme task
activeRequestsSemaphore.wait()
activeRequests.insert(urlSchemeTask.request)
activeRequestsSemaphore.signal()
// fetch data and send response
DispatchQueue.global(qos: .userInitiated).async {
// fetch data
self.dataFetchingSemaphore.wait()
let content = ZimFileService.shared.getURLContent(url: url)
self.dataFetchingSemaphore.signal()
// check the url scheme task is not stopped
self.activeRequestsSemaphore.wait()
guard let _ = self.activeRequests.remove(urlSchemeTask.request) else {
self.activeRequestsSemaphore.signal()
return
}
self.activeRequestsSemaphore.signal()
// assemble and send response
if let content = content,
let response = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": content.mime, "Content-Length": "\(content.length)"])
{
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(content.data)
urlSchemeTask.didFinish()
} else {
os_log("Resource not found for url: %s.", log: Log.URLSchemeHandler, type: .info, url.absoluteString)
urlSchemeTask.didFailWithError(URLError(.resourceUnavailable, userInfo: ["url": url]))
// fetch url content and return data to the task
self.urls.insert(url)
DispatchQueue.global(qos: .userInitiated).async {
let content = ZimFileService.shared.getURLContent(url: url)
self.queue.async {
guard self.urls.contains(url) else { return }
if let content = content,
let response = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": content.mime, "Content-Length": "\(content.length)"])
{
objCTryBlock {
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(content.data)
urlSchemeTask.didFinish()
}
} else {
os_log(
"Resource not found for url: %s.",
log: Log.URLSchemeHandler,
type: .info,
url.absoluteString
)
urlSchemeTask.didFailWithError(URLError(.resourceUnavailable, userInfo: ["url": url]))
}
self.urls.remove(url)
}
}
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
activeRequestsSemaphore.wait()
activeRequests.remove(urlSchemeTask.request)
activeRequestsSemaphore.signal()
queue.async {
guard let url = urlSchemeTask.request.url else { return }
self.urls.remove(url)
}
}
}

View File

@ -16,6 +16,7 @@ import Defaults
struct ZimFileDetail: View {
@Binding var url: URL?
@Default(.downloadUsingCellular) private var downloadUsingCellular
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject private var viewModel: ViewModel
@EnvironmentObject private var libraryViewModel: LibraryViewModel
@ObservedObject var zimFile: ZimFile
@ -126,6 +127,9 @@ struct ZimFileDetail: View {
"""),
primaryButton: .destructive(Text("Unlink")) {
LibraryOperations.unlink(zimFileID: zimFile.fileID)
#if os(iOS)
presentationMode.wrappedValue.dismiss()
#endif
},
secondaryButton: .cancel()
)
@ -141,6 +145,9 @@ struct ZimFileDetail: View {
message: Text("The zim file and all bookmarked articles linked to this zim file will be deleted."),
primaryButton: .destructive(Text("Delete")) {
LibraryOperations.delete(zimFileID: zimFile.fileID)
#if os(iOS)
presentationMode.wrappedValue.dismiss()
#endif
},
secondaryButton: .cancel()
)

View File

@ -80,7 +80,7 @@ class ZimFile: NSManagedObject, Identifiable {
@NSManaged var size: Int64
@NSManaged var downloadTask: DownloadTask?
@NSManaged var bookmarks: [Bookmark]
@NSManaged var bookmarks: Set<Bookmark>
static var openedPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "fileURLBookmark != nil"),

View File

@ -94,7 +94,7 @@ struct LibraryOperations {
/// Configure a zim file object based on its metadata.
static func configureZimFile(_ zimFile: ZimFile, metadata: ZimFileMetaData) {
zimFile.articleCount = metadata.articleCount.int64Value
zimFile.category = metadata.category
zimFile.category = (Category(rawValue: metadata.category) ?? .other).rawValue
zimFile.created = metadata.creationDate
zimFile.fileDescription = metadata.fileDescription
zimFile.fileID = metadata.fileID
@ -116,20 +116,22 @@ struct LibraryOperations {
//MARK: - Deletion
/// Unlink a zim file from library, and delete the file.
/// Unlink a zim file from library, delete associated bookmarks, and delete the file.
/// - Parameter zimFile: the zim file to delete
static func delete(zimFileID: UUID) {
guard let url = ZimFileService.shared.getFileURL(zimFileID: zimFileID) else { return }
defer { try? FileManager.default.removeItem(at: url) }
LibraryOperations.unlink(zimFileID: zimFileID)
}
/// Unlink a zim file from library, but don't delete the file.
/// Unlink a zim file from library, delete associated bookmarks, but don't delete the file.
/// - Parameter zimFile: the zim file to unlink
static func unlink(zimFileID: UUID) {
ZimFileService.shared.close(fileID: zimFileID)
Database.shared.container.performBackgroundTask { context in
context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
guard let zimFile = try? ZimFile.fetchRequest(fileID: zimFileID).execute().first else { return }
zimFile.bookmarks.forEach { context.delete($0) }
if zimFile.downloadURL == nil {
context.delete(zimFile)
} else {

View File

@ -59,14 +59,17 @@ extension SearchOperation {
}
}
// sort the results
guard !isCancelled else { return }
__results.sort { lhs, rhs in
guard let lhs = (lhs as? SearchResult)?.score?.doubleValue,
let rhs = (rhs as? SearchResult)?.score?.doubleValue else { return .orderedSame }
if lhs < rhs {
return .orderedAscending
} else if lhs > rhs {
return .orderedDescending
guard let lhs = lhs as? SearchResult,
let rhs = rhs as? SearchResult,
let lhsScore = lhs.score?.doubleValue,
let rhsScore = rhs.score?.doubleValue else { return .orderedSame }
if lhsScore != rhsScore {
return lhsScore < rhsScore ? .orderedAscending : .orderedDescending
} else if let lhsSnippet = lhs.snippet, let rhsSnippet = rhs.snippet {
return lhsSnippet.length > rhsSnippet.length ? .orderedAscending : .orderedDescending
} else {
return .orderedSame
}

View File

@ -75,6 +75,8 @@ struct RootViewV1: UIViewControllerRepresentable {
if isSearchActive {
searchBar.text = searchViewModel.searchText
} else {
searchBar.text = ""
// Triggers "AttributeGraph: cycle detected through attribute" if not dispatched (iOS 16.0 SDK)
DispatchQueue.main.async {
searchBar.resignFirstResponder()
@ -102,11 +104,6 @@ struct RootViewV1: UIViewControllerRepresentable {
rootView.isSearchActive = true
}
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
searchBar.text = ""
rootView.searchViewModel.searchText = ""
}
}
}
@ -114,7 +111,7 @@ private struct Content: View {
@Binding var isSearchActive: Bool
@Binding var url: URL?
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@EnvironmentObject var viewModel: ReadingViewModel
@EnvironmentObject var searchViewModel: SearchViewModel
var body: some View {
Group {
@ -155,6 +152,7 @@ private struct Content: View {
SettingsButton()
} else if isSearchActive {
Button("Cancel") {
searchViewModel.searchText = ""
isSearchActive = false
}
}

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key></key>
<string></string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>org.kiwix.library_refresh</string>
@ -32,6 +34,13 @@
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict/>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>

View File

@ -40,6 +40,8 @@
</array>
</dict>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>

View File

@ -44,7 +44,7 @@ class SearchViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDel
fetchedResultsController.delegate = self
// subscribers
searchSubscriber = Publishers.CombineLatest($searchText, $zimFiles)
searchSubscriber = Publishers.CombineLatest($searchText.removeDuplicates(), $zimFiles)
.map { [unowned self] searchText, zimFiles in
self.inProgress = true
return (searchText, zimFiles)

View File

@ -61,6 +61,8 @@ class ViewModel: NSObject, ObservableObject, WKNavigationDelegate, WKUIDelegate
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
let error = error as NSError
guard error.code != NSURLErrorCancelled else { return }
activeAlert = .articleFailedToLoad
}

View File

@ -132,6 +132,7 @@ struct LibraryButton: View {
struct MainArticleButton: View {
@Binding var url: URL?
@FetchRequest(sortDescriptors: [], predicate: ZimFile.openedPredicate) private var zimFiles: FetchedResults<ZimFile>
var body: some View {
Button {
@ -139,7 +140,9 @@ struct MainArticleButton: View {
url = ZimFileService.shared.getMainPageURL(zimFileID: zimFileID)
} label: {
Label("Main Article", systemImage: "house")
}.help("Show main article")
}
.disabled(zimFiles.isEmpty)
.help("Show main article")
}
}
@ -163,7 +166,9 @@ struct MainArticleMenu: View {
} primaryAction: {
let zimFileID = UUID(uuidString: url?.host ?? "")
url = ZimFileService.shared.getMainPageURL(zimFileID: zimFileID)
}.help("Show main article")
}
.disabled(zimFiles.isEmpty)
.help("Show main article")
}
}
@ -289,6 +294,7 @@ struct PageZoomButtons: View {
struct RandomArticleButton: View {
@Binding var url: URL?
@FetchRequest(sortDescriptors: [], predicate: ZimFile.openedPredicate) private var zimFiles: FetchedResults<ZimFile>
var body: some View {
Button {
@ -296,7 +302,9 @@ struct RandomArticleButton: View {
url = ZimFileService.shared.getRandomPageURL(zimFileID: zimFileID)
} label: {
Label("Random Article", systemImage: "die.face.5")
}.help("Show random article")
}
.disabled(zimFiles.isEmpty)
.help("Show random article")
}
}
@ -320,7 +328,9 @@ struct RandomArticleMenu: View {
} primaryAction: {
let zimFileID = UUID(uuidString: url?.host ?? "")
url = ZimFileService.shared.getRandomPageURL(zimFileID: zimFileID)
}.help("Show random article")
}
.disabled(zimFiles.isEmpty)
.help("Show random article")
}
}
@ -354,17 +364,17 @@ struct SidebarNavigationItemButtons: View {
@FocusedBinding(\.navigationItem) var navigationItem: NavigationItem??
var body: some View {
buildButtons([.reading, .bookmarks], keyboardShortcutOffset: 1)
buildButtons([.reading, .bookmarks], modifiers: [.command])
Divider()
buildButtons([.opened, .categories, .downloads, .new], keyboardShortcutOffset: 3)
buildButtons([.opened, .categories, .downloads, .new], modifiers: [.command, .control])
}
private func buildButtons(_ navigationItems: [NavigationItem], keyboardShortcutOffset: Int) -> some View {
private func buildButtons(_ navigationItems: [NavigationItem], modifiers: EventModifiers = []) -> some View {
ForEach(Array(navigationItems.enumerated()), id: \.element) { index, item in
Button(item.name) {
navigationItem = item
}
.keyboardShortcut(KeyEquivalent(Character("\(index + keyboardShortcutOffset)")))
.keyboardShortcut(KeyEquivalent(Character("\(index + 1)")), modifiers: modifiers)
.disabled(navigationItem == nil)
}
}