diff --git a/App/App_iOS.swift b/App/App_iOS.swift index 4cfafb09..eda7a982 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -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()) diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 40565deb..270d0239 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -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.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()) diff --git a/ViewModel/LibraryViewModel.swift b/ViewModel/LibraryViewModel.swift index fdaea279..9ed86201 100644 --- a/ViewModel/LibraryViewModel.swift +++ b/ViewModel/LibraryViewModel.swift @@ -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() 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() - - @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 diff --git a/Views/Library/Library.swift b/Views/Library/Library.swift index 2edb7950..e58ebc98 100644 --- a/Views/Library/Library.swift +++ b/Views/Library/Library.swift @@ -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: 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) diff --git a/Views/Library/ZimFileDetail.swift b/Views/Library/ZimFileDetail.swift index a7f3bf70..cf39fd2c 100644 --- a/Views/Library/ZimFileDetail.swift +++ b/Views/Library/ZimFileDetail.swift @@ -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 { diff --git a/Views/Library/ZimFilesCategories.swift b/Views/Library/ZimFilesCategories.swift index 013638c2..6b7088c1 100644 --- a/Views/Library/ZimFilesCategories.swift +++ b/Views/Library/ZimFilesCategories.swift @@ -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 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 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) } diff --git a/Views/Library/ZimFilesDownloads.swift b/Views/Library/ZimFilesDownloads.swift index 98b03ebd..b3be3dbd 100644 --- a/Views/Library/ZimFilesDownloads.swift +++ b/Views/Library/ZimFilesDownloads.swift @@ -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) } } diff --git a/Views/Library/ZimFilesNew.swift b/Views/Library/ZimFilesNew.swift index cadc2247..d1aaf511 100644 --- a/Views/Library/ZimFilesNew.swift +++ b/Views/Library/ZimFilesNew.swift @@ -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) } diff --git a/Views/Library/ZimFilesOpened.swift b/Views/Library/ZimFilesOpened.swift index 05b246b1..b4de7cd0 100644 --- a/Views/Library/ZimFilesOpened.swift +++ b/Views/Library/ZimFilesOpened.swift @@ -25,7 +25,7 @@ struct ZimFilesOpened: View { animation: .easeInOut ) private var zimFiles: FetchedResults @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 diff --git a/Views/ViewModifiers/CellBackground.swift b/Views/ViewModifiers/CellBackground.swift index ac9b4bdc..331cd8a7 100644 --- a/Views/ViewModifiers/CellBackground.swift +++ b/Views/ViewModifiers/CellBackground.swift @@ -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 } }