mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-08-03 12:37:15 -04:00
Split and rename viewModels
This commit is contained in:
parent
372cceccbb
commit
89ab9d7d42
@ -25,6 +25,7 @@ struct Kiwix: App {
|
||||
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@StateObject private var library = LibraryViewModel()
|
||||
@StateObject private var selection = SelectedZimFileViewModel(isMultiSelection: false)
|
||||
@StateObject private var navigation = NavigationViewModel()
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||
private let fileMonitor: DirectoryMonitor
|
||||
@ -53,6 +54,7 @@ struct Kiwix: App {
|
||||
.ignoresSafeArea()
|
||||
.environment(\.managedObjectContext, Database.shared.viewContext)
|
||||
.environmentObject(library)
|
||||
.environmentObject(selection)
|
||||
.environmentObject(navigation)
|
||||
.modifier(AlertHandler())
|
||||
.modifier(OpenFileHandler())
|
||||
|
@ -182,8 +182,12 @@ struct RootView: View {
|
||||
@State private var currentNavItem: MenuItem?
|
||||
@StateObject private var windowTracker = WindowTracker()
|
||||
@State private var paymentButtonLabel: PayWithApplePayButtonLabel?
|
||||
<<<<<<< HEAD
|
||||
@StateObject private var multiSelection = LibraryMultiSelectViewModel()
|
||||
var isSearchFocused: FocusState<Bool>.Binding
|
||||
=======
|
||||
@StateObject private var selection = SelectedZimFileViewModel(isMultiSelection: false)
|
||||
>>>>>>> f3ebab51 (Split and rename viewModels)
|
||||
|
||||
private let primaryItems: [MenuItem] = [.bookmarks]
|
||||
private let libraryItems: [MenuItem] = [.opened, .categories, .downloads, .new]
|
||||
@ -231,9 +235,9 @@ struct RootView: View {
|
||||
Bookmarks()
|
||||
.modifier(SearchFocused(isSearchFocused: isSearchFocused))
|
||||
case .opened:
|
||||
ZimFilesOpened(dismiss: nil)
|
||||
.environmentObject(multiSelection)
|
||||
.modifier(LibraryZimFileMultiSelectDetailSidePanel(viewModel: multiSelection))
|
||||
let multiSelection = SelectedZimFileViewModel(isMultiSelection: true)
|
||||
ZimFilesOpened(selection: multiSelection, dismiss: nil)
|
||||
.modifier(LibraryZimFileMultiSelectDetailSidePanel(selection: multiSelection))
|
||||
case .categories:
|
||||
ZimFilesCategories(dismiss: nil)
|
||||
.modifier(LibraryZimFileDetailSidePanel())
|
||||
|
@ -46,7 +46,7 @@ enum LibraryState {
|
||||
@MainActor
|
||||
final class SelectedZimFileViewModel: ObservableObject {
|
||||
let isMultiSelection: Bool
|
||||
@Published private(set) var selectedZimFile: ZimFile?
|
||||
@Published var selectedZimFile: ZimFile?
|
||||
@Published private(set) var selectedZimFiles = Set<ZimFile>()
|
||||
|
||||
init(isMultiSelection: Bool) {
|
||||
@ -70,7 +70,7 @@ final class SelectedZimFileViewModel: ObservableObject {
|
||||
selectedZimFiles = Set([zimFile])
|
||||
}
|
||||
|
||||
func resetSelection() {
|
||||
func reset() {
|
||||
selectedZimFile = nil
|
||||
selectedZimFiles.removeAll()
|
||||
}
|
||||
@ -83,31 +83,7 @@ final class SelectedZimFileViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
final class LibraryMultiSelectViewModel: ObservableObject {
|
||||
@Published private(set) var selectedZimFiles = Set<ZimFile>()
|
||||
|
||||
@MainActor
|
||||
func toggleMultiSelect(of zimFile: ZimFile) {
|
||||
if selectedZimFiles.contains(zimFile) {
|
||||
selectedZimFiles.remove(zimFile)
|
||||
} else {
|
||||
selectedZimFiles.insert(zimFile)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func singleSelect(zimFile: ZimFile) {
|
||||
selectedZimFiles = Set([zimFile])
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func resetSelection() {
|
||||
selectedZimFiles.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
final class LibraryViewModel: ObservableObject {
|
||||
@Published var selectedZimFile: ZimFile?
|
||||
@MainActor @Published private(set) var error: Error?
|
||||
/// Note: due to multiple instances of LibraryViewModel,
|
||||
/// this `state` should not be changed directly, modify the `process.state` instead
|
||||
|
@ -96,7 +96,7 @@ struct Library_Previews: PreviewProvider {
|
||||
|
||||
/// On macOS, adds a panel to the right of the modified view to show zim file detail.
|
||||
struct LibraryZimFileMultiSelectDetailSidePanel: ViewModifier {
|
||||
@ObservedObject var viewModel: LibraryMultiSelectViewModel
|
||||
@ObservedObject var selection: SelectedZimFileViewModel
|
||||
@State private var isPresentingUnlinkAlert: Bool = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
@ -105,22 +105,22 @@ struct LibraryZimFileMultiSelectDetailSidePanel: ViewModifier {
|
||||
content.safeAreaInset(edge: .trailing, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Divider()
|
||||
switch viewModel.selectedZimFiles.count {
|
||||
switch selection.selectedZimFiles.count {
|
||||
case 0:
|
||||
Message(text: LocalString.library_zim_file_details_side_panel_message)
|
||||
.background(.thickMaterial)
|
||||
case 1:
|
||||
ZimFileDetail(zimFile: viewModel.selectedZimFiles.first!, dismissParent: nil)
|
||||
ZimFileDetail(zimFile: selection.selectedZimFiles.first!, dismissParent: nil)
|
||||
default:
|
||||
Action(title: LocalString.zim_file_action_unlink_title, isDestructive: true) {
|
||||
isPresentingUnlinkAlert = true
|
||||
}.alert(isPresented: $isPresentingUnlinkAlert) {
|
||||
Alert(
|
||||
title: Text(LocalString.zim_file_action_unlink_title + " " + "\(viewModel.selectedZimFiles.count)"),
|
||||
title: Text(LocalString.zim_file_action_unlink_title + " " + "\(selection.selectedZimFiles.count)"),
|
||||
message: Text(LocalString.zim_file_action_unlink_message),
|
||||
primaryButton: .destructive(Text(LocalString.zim_file_action_unlink_button_title)) {
|
||||
Task {
|
||||
for zimFile in viewModel.selectedZimFiles {
|
||||
for zimFile in selection.selectedZimFiles {
|
||||
await LibraryOperations.unlink(zimFileID: zimFile.fileID)
|
||||
}
|
||||
}
|
||||
@ -131,14 +131,14 @@ struct LibraryZimFileMultiSelectDetailSidePanel: ViewModifier {
|
||||
}
|
||||
}.frame(width: 275).background(.ultraThinMaterial)
|
||||
}
|
||||
}.onAppear { viewModel.resetSelection() }
|
||||
}.onAppear { selection.reset() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// On macOS, adds a panel to the right of the modified view to show zim file detail.
|
||||
struct LibraryZimFileDetailSidePanel: ViewModifier {
|
||||
@EnvironmentObject private var viewModel: LibraryViewModel
|
||||
@EnvironmentObject private var selection: SelectedZimFileViewModel
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
@ -146,7 +146,7 @@ struct LibraryZimFileDetailSidePanel: ViewModifier {
|
||||
content.safeAreaInset(edge: .trailing, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Divider()
|
||||
if let zimFile = viewModel.selectedZimFile {
|
||||
if let zimFile = selection.selectedZimFile {
|
||||
ZimFileDetail(zimFile: zimFile, dismissParent: nil)
|
||||
} else {
|
||||
Message(text: LocalString.library_zim_file_details_side_panel_message)
|
||||
@ -154,7 +154,7 @@ struct LibraryZimFileDetailSidePanel: ViewModifier {
|
||||
}
|
||||
}.frame(width: 275).background(.ultraThinMaterial)
|
||||
}
|
||||
}.onAppear { viewModel.selectedZimFile = nil }
|
||||
}.onAppear { selection.selectedZimFile = nil }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -162,41 +162,39 @@ 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<Content: View>: View {
|
||||
@EnvironmentObject private var viewModel: LibraryViewModel
|
||||
@ObservedObject var selection: SelectedZimFileViewModel
|
||||
|
||||
private let content: Content
|
||||
private let zimFile: ZimFile
|
||||
/// iOS only
|
||||
private let dismiss: (() -> Void)?
|
||||
/// macOS only
|
||||
private let onMultiSelected: ((Bool) -> Void)?
|
||||
|
||||
init(
|
||||
@ViewBuilder content: () -> Content,
|
||||
zimFile: ZimFile,
|
||||
onMultiSelected: ((Bool) -> Void)? = nil,
|
||||
selection: SelectedZimFileViewModel,
|
||||
dismiss: (() -> Void)? = nil
|
||||
) {
|
||||
self.content = content()
|
||||
self.zimFile = zimFile
|
||||
self.onMultiSelected = onMultiSelected
|
||||
self.selection = selection
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(macOS)
|
||||
if let onMultiSelected {
|
||||
if selection.isMultiSelection {
|
||||
content
|
||||
.gesture(TapGesture().modifiers(.command).onEnded({ value in
|
||||
onMultiSelected(true)
|
||||
selection.toggleMultiSelect(of: zimFile)
|
||||
}))
|
||||
.gesture(TapGesture().onEnded({ _ in
|
||||
onMultiSelected(false)
|
||||
selection.singleSelect(zimFile: zimFile)
|
||||
}))
|
||||
} else {
|
||||
content.onTapGesture {
|
||||
viewModel.selectedZimFile = zimFile
|
||||
selection.singleSelect(zimFile: zimFile)
|
||||
}
|
||||
}
|
||||
#elseif os(iOS)
|
||||
|
@ -282,14 +282,14 @@ private struct FileLocator: ViewModifier {
|
||||
|
||||
private struct DownloadTaskDetail: View {
|
||||
@ObservedObject var downloadZimFile: ZimFile
|
||||
@EnvironmentObject var viewModel: LibraryViewModel
|
||||
@EnvironmentObject var selection: SelectedZimFileViewModel
|
||||
@State private var downloadState = DownloadState.empty()
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
Action(title: LocalString.zim_file_download_task_action_title_cancel, isDestructive: true) {
|
||||
DownloadService.shared.cancel(zimFileID: downloadZimFile.fileID)
|
||||
viewModel.selectedZimFile = nil
|
||||
selection.reset()
|
||||
}
|
||||
if let error = downloadZimFile.downloadTask?.error {
|
||||
if downloadState.resumeData != nil {
|
||||
|
@ -105,6 +105,7 @@ private struct CategoryGrid: View {
|
||||
@Binding var searchText: String
|
||||
@Default(.libraryLanguageCodes) private var languageCodes
|
||||
@EnvironmentObject private var viewModel: LibraryViewModel
|
||||
@EnvironmentObject private var selection: SelectedZimFileViewModel
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@SectionedFetchRequest private var sections: SectionedFetchResults<String, ZimFile>
|
||||
private let dismiss: (() -> Void)? // iOS only
|
||||
@ -142,6 +143,7 @@ private struct CategoryGrid: View {
|
||||
LibraryZimFileContext(
|
||||
content: { ZimFileCell(zimFile, prominent: .size) },
|
||||
zimFile: zimFile,
|
||||
selection: selection,
|
||||
dismiss: dismiss)
|
||||
}
|
||||
} else {
|
||||
@ -150,6 +152,7 @@ private struct CategoryGrid: View {
|
||||
LibraryZimFileContext(
|
||||
content: { ZimFileCell(zimFile, prominent: .size) },
|
||||
zimFile: zimFile,
|
||||
selection: selection,
|
||||
dismiss: dismiss)
|
||||
}
|
||||
} header: {
|
||||
@ -173,7 +176,7 @@ private struct CategoryGrid: View {
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchText)
|
||||
.onChange(of: category) { _ in viewModel.selectedZimFile = nil }
|
||||
.onChange(of: category) { _ in selection.reset() }
|
||||
.onChange(of: searchText) { _ in
|
||||
sections.nsPredicate = ZimFilesCategory.buildPredicate(category: category, searchText: searchText)
|
||||
}
|
||||
@ -211,6 +214,7 @@ private struct CategoryList: View {
|
||||
@Binding var searchText: String
|
||||
@Default(.libraryLanguageCodes) private var languageCodes
|
||||
@EnvironmentObject private var viewModel: LibraryViewModel
|
||||
@EnvironmentObject private var selection: SelectedZimFileViewModel
|
||||
@FetchRequest private var zimFiles: FetchedResults<ZimFile>
|
||||
private let dismiss: (() -> Void)?
|
||||
|
||||
@ -247,10 +251,11 @@ private struct CategoryList: View {
|
||||
Message(text: LocalString.zim_file_category_section_empty_message)
|
||||
}
|
||||
} else {
|
||||
List(zimFiles, id: \.self, selection: $viewModel.selectedZimFile) { zimFile in
|
||||
List(zimFiles, id: \.self, selection: $selection.selectedZimFile) { zimFile in
|
||||
LibraryZimFileContext(
|
||||
content: { ZimFileRow(zimFile) },
|
||||
zimFile: zimFile,
|
||||
selection: selection,
|
||||
dismiss: dismiss)
|
||||
}
|
||||
#if os(macOS)
|
||||
@ -261,7 +266,7 @@ private struct CategoryList: View {
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchText)
|
||||
.onChange(of: category) { _ in viewModel.selectedZimFile = nil }
|
||||
.onChange(of: category) { _ in selection.reset() }
|
||||
.onChange(of: searchText) { _ in
|
||||
zimFiles.nsPredicate = ZimFilesCategory.buildPredicate(category: category, searchText: searchText)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import SwiftUI
|
||||
|
||||
/// A grid of zim files that are being downloaded.
|
||||
struct ZimFilesDownloads: View {
|
||||
@EnvironmentObject var selection: SelectedZimFileViewModel
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \DownloadTask.created, ascending: false)],
|
||||
@ -39,6 +40,7 @@ struct ZimFilesDownloads: View {
|
||||
LibraryZimFileContext(
|
||||
content: { DownloadTaskCell(zimFile) },
|
||||
zimFile: zimFile,
|
||||
selection: selection,
|
||||
dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +97,7 @@ private final class ViewModel: ObservableObject {
|
||||
/// A grid of zim files that are newly available.
|
||||
struct ZimFilesNew: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@EnvironmentObject private var selection: SelectedZimFileViewModel
|
||||
@EnvironmentObject var library: LibraryViewModel
|
||||
@Default(.libraryLanguageCodes) private var languageCodes
|
||||
@StateObject private var viewModel = ViewModel()
|
||||
@ -115,6 +116,7 @@ struct ZimFilesNew: View {
|
||||
ZimFileCell(zimFile, prominent: .name)
|
||||
},
|
||||
zimFile: zimFile,
|
||||
selection: selection,
|
||||
dismiss: dismiss)
|
||||
.transition(AnyTransition.opacity)
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ struct ZimFilesOpened: View {
|
||||
animation: .easeInOut
|
||||
) private var zimFiles: FetchedResults<ZimFile>
|
||||
@State private var isFileImporterPresented = false
|
||||
@EnvironmentObject private var viewModel: SelectedZimFileViewModel
|
||||
@ObservedObject var selection: SelectedZimFileViewModel
|
||||
let dismiss: (() -> Void)? // iOS only
|
||||
|
||||
var body: some View {
|
||||
@ -35,28 +35,16 @@ struct ZimFilesOpened: View {
|
||||
spacing: 12
|
||||
) {
|
||||
ForEach(zimFiles) { zimFile in
|
||||
let multiSelected: ((Bool) -> Void)? = if viewModel.isMultiSelection {
|
||||
{ [zimFile] isSelectionMulti in
|
||||
if isSelectionMulti {
|
||||
viewModel.toggleMultiSelect(of: zimFile)
|
||||
} else {
|
||||
viewModel.singleSelect(zimFile: zimFile)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
LibraryZimFileContext(
|
||||
content: {
|
||||
ZimFileCell(
|
||||
zimFile,
|
||||
prominent: .name,
|
||||
isSelected: viewModel.isSelected(zimFile)
|
||||
isSelected: selection.isSelected(zimFile)
|
||||
)
|
||||
},
|
||||
zimFile: zimFile,
|
||||
onMultiSelected: multiSelected,
|
||||
selection: selection,
|
||||
dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
@ -70,9 +58,9 @@ struct ZimFilesOpened: View {
|
||||
}
|
||||
.onChange(of: zimFiles.count) { _ in
|
||||
if let firstZimFile = zimFiles.first {
|
||||
viewModel.singleSelect(zimFile: firstZimFile)
|
||||
selection.singleSelect(zimFile: firstZimFile)
|
||||
} else {
|
||||
viewModel.resetSelection()
|
||||
selection.reset()
|
||||
}
|
||||
}
|
||||
// not using OpenFileButton here, because it does not work on iOS/iPadOS 15 when this view is in a modal
|
||||
|
@ -18,19 +18,17 @@ import SwiftUI
|
||||
enum CellBackground {
|
||||
#if os(macOS)
|
||||
private static let normal: Color = Color(nsColor: NSColor.controlBackgroundColor)
|
||||
private static let hover: Color = Color(nsColor: NSColor.selectedControlColor)
|
||||
private static let selected: Color = Color(nsColor: NSColor.selectedControlColor)
|
||||
private static let hoverSelected: Color = Color(nsColor: NSColor.selectedControlColor)
|
||||
#else
|
||||
private static let normal: Color = .secondaryBackground
|
||||
private static let hover: Color = .tertiaryBackground
|
||||
private static let selected: Color = .tertiaryBackground
|
||||
#endif
|
||||
|
||||
static func colorFor(isHovering: Bool, isSelected: Bool = false) -> Color {
|
||||
if isSelected {
|
||||
isHovering ? hoverSelected : selected
|
||||
isHovering ? selected.opacity(0.75) : selected
|
||||
} else {
|
||||
isHovering ? hover : normal
|
||||
isHovering ? selected.opacity(0.5) : normal
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user