mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-21 02:21:39 -04:00
Merge pull request #1117 from kiwix/1116-ios-library-scroll-no-viewmodifier-fix
Fix library scroll issue - part 1
This commit is contained in:
commit
c5c76cb14f
36
Views/BuildingBlocks/ArticleActions.swift
Normal file
36
Views/BuildingBlocks/ArticleActions.swift
Normal file
@ -0,0 +1,36 @@
|
||||
// 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/.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ArticleActions: View {
|
||||
|
||||
let zimFileID: UUID
|
||||
|
||||
var body: some View {
|
||||
AsyncButton {
|
||||
guard let url = await ZimFileService.shared.getMainPageURL(zimFileID: zimFileID) else { return }
|
||||
NotificationCenter.openURL(url, inNewTab: true)
|
||||
} label: {
|
||||
Label(LocalString.library_zim_file_context_main_page_label, systemImage: "house")
|
||||
}
|
||||
AsyncButton {
|
||||
guard let url = await ZimFileService.shared.getRandomPageURL(zimFileID: zimFileID) else { return }
|
||||
NotificationCenter.openURL(url, inNewTab: true)
|
||||
} label: {
|
||||
Label(LocalString.library_zim_file_context_random_label, systemImage: "die.face.5")
|
||||
}
|
||||
}
|
||||
}
|
35
Views/BuildingBlocks/CopyPasteMenu.swift
Normal file
35
Views/BuildingBlocks/CopyPasteMenu.swift
Normal file
@ -0,0 +1,35 @@
|
||||
// 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/.
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct CopyPasteMenu: View {
|
||||
|
||||
let downloadURL: URL
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
#if os(macOS)
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(downloadURL.absoluteString, forType: .string)
|
||||
#elseif os(iOS)
|
||||
UIPasteboard.general.setValue(downloadURL.absoluteString, forPasteboardType: UTType.url.identifier)
|
||||
#endif
|
||||
} label: {
|
||||
Label(LocalString.library_zim_file_context_copy_url, systemImage: "doc.on.doc")
|
||||
}
|
||||
}
|
||||
}
|
@ -112,70 +112,47 @@ struct LibraryZimFileDetailSidePanel: ViewModifier {
|
||||
|
||||
/// On macOS, converts the modified view to a Button that modifies the currently selected zim file
|
||||
/// On iOS, converts the modified view to a NavigationLink that goes to the zim file detail.
|
||||
struct LibraryZimFileContext: ViewModifier {
|
||||
struct LibraryZimFileContext<Content: View>: View {
|
||||
@EnvironmentObject private var viewModel: LibraryViewModel
|
||||
@EnvironmentObject private var navigation: NavigationViewModel
|
||||
|
||||
let zimFile: ZimFile
|
||||
let dismiss: (() -> Void)? // iOS only
|
||||
|
||||
init(zimFile: ZimFile, dismiss: (() -> Void)?) {
|
||||
|
||||
private let content: Content
|
||||
private let zimFile: ZimFile
|
||||
/// iOS only
|
||||
private let dismiss: (() -> Void)?
|
||||
|
||||
init(
|
||||
@ViewBuilder content: () -> Content,
|
||||
zimFile: ZimFile,
|
||||
dismiss: (() -> Void)? = nil
|
||||
) {
|
||||
self.content = content()
|
||||
self.zimFile = zimFile
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
Button {
|
||||
viewModel.selectedZimFile = zimFile
|
||||
} label: {
|
||||
content
|
||||
}.buttonStyle(.plain)
|
||||
#elseif os(iOS)
|
||||
#elseif os(iOS)
|
||||
NavigationLink {
|
||||
ZimFileDetail(zimFile: zimFile, dismissParent: dismiss)
|
||||
} label: {
|
||||
content
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}.contextMenu {
|
||||
if zimFile.fileURLBookmark != nil, !zimFile.isMissing {
|
||||
Section { articleActions }
|
||||
Section { ArticleActions(zimFileID: zimFile.fileID) }
|
||||
}
|
||||
Section { supplementaryActions }
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var articleActions: some View {
|
||||
AsyncButton {
|
||||
guard let url = await ZimFileService.shared.getMainPageURL(zimFileID: zimFile.fileID) else { return }
|
||||
NotificationCenter.openURL(url, inNewTab: true)
|
||||
} label: {
|
||||
Label(LocalString.library_zim_file_context_main_page_label, systemImage: "house")
|
||||
}
|
||||
AsyncButton {
|
||||
guard let url = await ZimFileService.shared.getRandomPageURL(zimFileID: zimFile.fileID) else { return }
|
||||
NotificationCenter.openURL(url, inNewTab: true)
|
||||
} label: {
|
||||
Label(LocalString.library_zim_file_context_random_label, systemImage: "die.face.5")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var supplementaryActions: some View {
|
||||
if let downloadURL = zimFile.downloadURL {
|
||||
Button {
|
||||
#if os(macOS)
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(downloadURL.absoluteString, forType: .string)
|
||||
#elseif os(iOS)
|
||||
UIPasteboard.general.setValue(downloadURL.absoluteString, forPasteboardType: UTType.url.identifier)
|
||||
#endif
|
||||
} label: {
|
||||
Label(LocalString.library_zim_file_context_copy_url, systemImage: "doc.on.doc")
|
||||
if let downloadURL = zimFile.downloadURL {
|
||||
Section { CopyPasteMenu(downloadURL: downloadURL) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -139,14 +139,18 @@ private struct CategoryGrid: View {
|
||||
ForEach(sections) { section in
|
||||
if sections.count <= 1 {
|
||||
ForEach(section) { zimFile in
|
||||
ZimFileCell(zimFile, prominent: .size)
|
||||
.modifier(LibraryZimFileContext(zimFile: zimFile, dismiss: dismiss))
|
||||
LibraryZimFileContext(
|
||||
content: { ZimFileCell(zimFile, prominent: .size) },
|
||||
zimFile: zimFile,
|
||||
dismiss: dismiss)
|
||||
}
|
||||
} else {
|
||||
Section {
|
||||
ForEach(section) { zimFile in
|
||||
ZimFileCell(zimFile, prominent: .size)
|
||||
.modifier(LibraryZimFileContext(zimFile: zimFile, dismiss: dismiss))
|
||||
LibraryZimFileContext(
|
||||
content: { ZimFileCell(zimFile, prominent: .size) },
|
||||
zimFile: zimFile,
|
||||
dismiss: dismiss)
|
||||
}
|
||||
} header: {
|
||||
SectionHeader(
|
||||
@ -244,8 +248,10 @@ private struct CategoryList: View {
|
||||
}
|
||||
} else {
|
||||
List(zimFiles, id: \.self, selection: $viewModel.selectedZimFile) { zimFile in
|
||||
ZimFileRow(zimFile)
|
||||
.modifier(LibraryZimFileContext(zimFile: zimFile, dismiss: dismiss))
|
||||
LibraryZimFileContext(
|
||||
content: { ZimFileRow(zimFile) },
|
||||
zimFile: zimFile,
|
||||
dismiss: dismiss)
|
||||
}
|
||||
#if os(macOS)
|
||||
.listStyle(.inset)
|
||||
|
@ -35,10 +35,11 @@ struct ZimFilesDownloads: View {
|
||||
alignment: .leading,
|
||||
spacing: 12
|
||||
) {
|
||||
ForEach(downloadTasks) { downloadTask in
|
||||
if let zimFile = downloadTask.zimFile {
|
||||
DownloadTaskCell(zimFile).modifier(LibraryZimFileContext(zimFile: zimFile, dismiss: dismiss))
|
||||
}
|
||||
ForEach(downloadTasks.compactMap(\.zimFile)) { zimFile in
|
||||
LibraryZimFileContext(
|
||||
content: { DownloadTaskCell(zimFile) },
|
||||
zimFile: zimFile,
|
||||
dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
.modifier(GridCommon())
|
||||
|
@ -14,26 +14,93 @@
|
||||
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import Defaults
|
||||
|
||||
private final class ViewModel: ObservableObject {
|
||||
|
||||
@Published private(set) var zimFiles: [ZimFile] = []
|
||||
|
||||
private var languageCodes = Set<String>()
|
||||
private var searchText: String = ""
|
||||
|
||||
private let sortDescriptors = [
|
||||
NSSortDescriptor(keyPath: \ZimFile.created, ascending: false),
|
||||
NSSortDescriptor(keyPath: \ZimFile.name, ascending: true),
|
||||
NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)
|
||||
]
|
||||
|
||||
func update(languageCodes: Set<String>) {
|
||||
guard languageCodes != self.languageCodes else { return }
|
||||
self.languageCodes = languageCodes
|
||||
Task {
|
||||
await update()
|
||||
}
|
||||
}
|
||||
|
||||
func update(searchText: String) {
|
||||
guard searchText != self.searchText else { return }
|
||||
self.searchText = searchText
|
||||
Task {
|
||||
await update()
|
||||
}
|
||||
}
|
||||
|
||||
func update() async {
|
||||
let searchText = self.searchText
|
||||
let languageCodes = self.languageCodes
|
||||
let newZimFiles: [ZimFile] = await withCheckedContinuation { continuation in
|
||||
Database.shared.performBackgroundTask { context in
|
||||
let predicate: NSPredicate = Self.buildPredicate(
|
||||
searchText: searchText,
|
||||
languageCodes: languageCodes
|
||||
)
|
||||
if let results = try? context.fetch(
|
||||
ZimFile.fetchRequest(
|
||||
predicate: predicate,
|
||||
sortDescriptors: self.sortDescriptors
|
||||
)
|
||||
) {
|
||||
continuation.resume(returning: results)
|
||||
} else {
|
||||
continuation.resume(returning: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
await MainActor.run {
|
||||
withAnimation(.easeInOut) {
|
||||
self.zimFiles = newZimFiles
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func buildPredicate(searchText: String, languageCodes: Set<String>) -> NSPredicate {
|
||||
var predicates = [
|
||||
NSPredicate(format: "languageCode IN %@", languageCodes),
|
||||
NSPredicate(format: "requiresServiceWorkers == false")
|
||||
]
|
||||
if let aMonthAgo = Calendar.current.date(byAdding: .month, value: -3, to: Date()) {
|
||||
predicates.append(NSPredicate(format: "created > %@", aMonthAgo as CVarArg))
|
||||
}
|
||||
if !searchText.isEmpty {
|
||||
predicates.append(
|
||||
NSCompoundPredicate(orPredicateWithSubpredicates: [
|
||||
NSPredicate(format: "name CONTAINS[cd] %@", searchText),
|
||||
NSPredicate(format: "fileDescription CONTAINS[cd] %@", searchText)
|
||||
])
|
||||
)
|
||||
}
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// A grid of zim files that are newly available.
|
||||
struct ZimFilesNew: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@EnvironmentObject var viewModel: LibraryViewModel
|
||||
@EnvironmentObject var library: LibraryViewModel
|
||||
@Default(.libraryLanguageCodes) private var languageCodes
|
||||
@FetchRequest(
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \ZimFile.created, ascending: false),
|
||||
NSSortDescriptor(keyPath: \ZimFile.name, ascending: true),
|
||||
NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)
|
||||
],
|
||||
animation: .easeInOut
|
||||
) private var zimFiles: FetchedResults<ZimFile>
|
||||
@StateObject private var viewModel = ViewModel()
|
||||
@State private var searchText = ""
|
||||
private var filterPredicate: NSPredicate {
|
||||
ZimFilesNew.buildPredicate(searchText: searchText)
|
||||
}
|
||||
let dismiss: (() -> Void)? // iOS only
|
||||
|
||||
var body: some View {
|
||||
@ -42,9 +109,14 @@ struct ZimFilesNew: View {
|
||||
alignment: .leading,
|
||||
spacing: 12
|
||||
) {
|
||||
ForEach(zimFiles.filter { filterPredicate.evaluate(with: $0) }) { zimFile in
|
||||
ZimFileCell(zimFile, prominent: .name)
|
||||
.modifier(LibraryZimFileContext(zimFile: zimFile, dismiss: dismiss))
|
||||
ForEach(viewModel.zimFiles, id: \.fileID) { zimFile in
|
||||
LibraryZimFileContext(
|
||||
content: {
|
||||
ZimFileCell(zimFile, prominent: .name)
|
||||
},
|
||||
zimFile: zimFile,
|
||||
dismiss: dismiss)
|
||||
.transition(AnyTransition.opacity)
|
||||
}
|
||||
}
|
||||
.modifier(GridCommon())
|
||||
@ -52,11 +124,19 @@ struct ZimFilesNew: View {
|
||||
.navigationTitle(NavigationItem.new.name)
|
||||
.searchable(text: $searchText)
|
||||
.onAppear {
|
||||
viewModel.start(isUserInitiated: false)
|
||||
viewModel.update(searchText: searchText)
|
||||
viewModel.update(languageCodes: languageCodes)
|
||||
library.start(isUserInitiated: false)
|
||||
}
|
||||
.onChange(of: searchText) { newSearchText in
|
||||
viewModel.update(searchText: newSearchText)
|
||||
}
|
||||
.onChange(of: languageCodes) { newLanguageCodes in
|
||||
viewModel.update(languageCodes: newLanguageCodes)
|
||||
}
|
||||
.overlay {
|
||||
if zimFiles.isEmpty {
|
||||
switch viewModel.state {
|
||||
if viewModel.zimFiles.isEmpty {
|
||||
switch library.state {
|
||||
case .inProgress:
|
||||
Message(text: LocalString.zim_file_catalog_fetching_message)
|
||||
case .error:
|
||||
@ -80,14 +160,14 @@ struct ZimFilesNew: View {
|
||||
}
|
||||
#endif
|
||||
ToolbarItem {
|
||||
if viewModel.state == .inProgress {
|
||||
if library.state == .inProgress {
|
||||
ProgressView()
|
||||
#if os(macOS)
|
||||
.scaleEffect(0.5)
|
||||
#endif
|
||||
} else {
|
||||
Button {
|
||||
viewModel.start(isUserInitiated: true)
|
||||
library.start(isUserInitiated: true)
|
||||
} label: {
|
||||
Label(LocalString.zim_file_new_button_refresh,
|
||||
systemImage: "arrow.triangle.2.circlepath.circle")
|
||||
@ -96,25 +176,6 @@ struct ZimFilesNew: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func buildPredicate(searchText: String) -> NSPredicate {
|
||||
var predicates = [
|
||||
NSPredicate(format: "languageCode IN %@", Defaults[.libraryLanguageCodes]),
|
||||
NSPredicate(format: "requiresServiceWorkers == false")
|
||||
]
|
||||
if let aMonthAgo = Calendar.current.date(byAdding: .month, value: -3, to: Date()) {
|
||||
predicates.append(NSPredicate(format: "created > %@", aMonthAgo as CVarArg))
|
||||
}
|
||||
if !searchText.isEmpty {
|
||||
predicates.append(
|
||||
NSCompoundPredicate(orPredicateWithSubpredicates: [
|
||||
NSPredicate(format: "name CONTAINS[cd] %@", searchText),
|
||||
NSPredicate(format: "fileDescription CONTAINS[cd] %@", searchText)
|
||||
])
|
||||
)
|
||||
}
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13.0, iOS 16.0, *)
|
||||
|
@ -35,8 +35,10 @@ struct ZimFilesOpened: View {
|
||||
spacing: 12
|
||||
) {
|
||||
ForEach(zimFiles) { zimFile in
|
||||
ZimFileCell(zimFile, prominent: .name).modifier(LibraryZimFileContext(zimFile: zimFile,
|
||||
dismiss: self.dismiss))
|
||||
LibraryZimFileContext(
|
||||
content: { ZimFileCell(zimFile, prominent: .name) },
|
||||
zimFile: zimFile,
|
||||
dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
.modifier(GridCommon(edges: .all))
|
||||
|
Loading…
x
Reference in New Issue
Block a user