diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift index aea2dd6ea..64c47eabd 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift @@ -279,6 +279,8 @@ class ItemEditorViewModel: ViewModel, Stateful, Eventful { // MARK: - Reorder Elements (To Be Overridden) + // TODO: should instead move to an index-based self insertion + // instead of replacement func reorderComponents(_ tags: [Element]) async throws { fatalError("This method should be overridden in subclasses") } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index e3c6738dc..2d5118941 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2901,8 +2901,8 @@ 4EFAC1312D1E373B00E40880 /* AddServerUserAccessTagsView */ = { isa = PBXGroup; children = ( - 4EFAC1322D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift */, 4EFAC1342D1FB19700E40880 /* Components */, + 4EFAC1322D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift */, ); path = AddServerUserAccessTagsView; sourceTree = ""; diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/AddServerUserAccessTagsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/AddServerUserAccessTagsView.swift index b0532f859..954155579 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/AddServerUserAccessTagsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/AddServerUserAccessTagsView.swift @@ -19,7 +19,7 @@ struct AddServerUserAccessTagsView: View { @ObservedObject private var viewModel: ServerUserAdminViewModel - @ObservedObject + @StateObject private var tagViewModel: TagEditorViewModel // MARK: - Access Tag Variables @@ -31,11 +31,6 @@ struct AddServerUserAccessTagsView: View { @State private var access: Bool = false - // MARK: - Trie Data Loaded - - @State - private var loaded: Bool = false - // MARK: - Error State @State @@ -65,7 +60,7 @@ struct AddServerUserAccessTagsView: View { init(viewModel: ServerUserAdminViewModel) { self.viewModel = viewModel self.tempPolicy = viewModel.user.policy! - self.tagViewModel = TagEditorViewModel(item: .init()) + self._tagViewModel = StateObject(wrappedValue: TagEditorViewModel(item: .init())) } // MARK: - Body @@ -126,7 +121,6 @@ struct AddServerUserAccessTagsView: View { case .updated: break case .loaded: - loaded = true tagViewModel.send(.search(tempTag)) case let .error(eventError): UIDevice.feedback(.error) @@ -149,7 +143,7 @@ struct AddServerUserAccessTagsView: View { SearchResultsSection( tag: $tempTag, - population: tagViewModel.matches, + tags: tagViewModel.matches, isSearching: tagViewModel.backgroundStates.contains(.searching) ) } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/Components/AccessTagSearchResultsSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/Components/AccessTagSearchResultsSection.swift index 89d3f58b2..80c5a0bc1 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/Components/AccessTagSearchResultsSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/Components/AccessTagSearchResultsSection.swift @@ -20,7 +20,7 @@ extension AddServerUserAccessTagsView { // MARK: - Element Search Variables - let population: [String] + let tags: [String] let isSearching: Bool // MARK: - Body @@ -28,26 +28,25 @@ extension AddServerUserAccessTagsView { var body: some View { if tag.isNotEmpty { Section { - if population.isNotEmpty { + if tags.isNotEmpty { resultsView - .animation(.easeInOut, value: population.count) } else if !isSearching { noResultsView - .transition(.opacity) - .animation(.easeInOut, value: population.count) } } header: { HStack { Text(L10n.existingItems) + if isSearching { - DelayedProgressView() + ProgressView() } else { Text("-") - Text(population.count.description) + + Text(tags.count, format: .number) } } - .animation(.easeInOut, value: isSearching) } + .animation(.linear(duration: 0.2), value: tags) } } @@ -56,23 +55,17 @@ extension AddServerUserAccessTagsView { private var noResultsView: some View { Text(L10n.none) .foregroundStyle(.secondary) - .frame(maxWidth: .infinity, alignment: .center) } // MARK: - Results View private var resultsView: some View { - ForEach(population, id: \.self) { result in - Button { + ForEach(tags, id: \.self) { result in + Button(result) { tag = result - } label: { - Text(result) - .frame(maxWidth: .infinity, alignment: .leading) } .foregroundStyle(.primary) .disabled(tag == result) - .transition(.opacity.combined(with: .move(edge: .top))) - .animation(.easeInOut, value: population.count) } } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/Components/TagInput.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/Components/TagInput.swift index afced4e0e..76a820e1c 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/Components/TagInput.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/AddServerUserAccessTagsView/Components/TagInput.swift @@ -15,6 +15,9 @@ extension AddServerUserAccessTagsView { // MARK: - Element Variables + @FocusState + private var isTagFocused: Bool + @Binding var access: Bool @Binding @@ -26,40 +29,34 @@ extension AddServerUserAccessTagsView { // MARK: - Body var body: some View { - tagView - } - - // MARK: - Tag View - - @ViewBuilder - private var tagView: some View { - Section { - Picker(L10n.access, selection: $access) { - Text(L10n.allowed).tag(true) - Text(L10n.blocked).tag(false) - } - // TODO: Enable on 10.10 - .disabled(true) - } header: { - Text(L10n.access) - } footer: { - LearnMoreButton(L10n.accessTags) { - TextPair( - title: L10n.allowed, - subtitle: L10n.accessTagAllowDescription - ) - TextPair( - title: L10n.blocked, - subtitle: L10n.accessTagBlockDescription - ) - } - } + // TODO: Enable on 10.10 +// Section { +// Picker(L10n.access, selection: $access) { +// Text(L10n.allowed).tag(true) +// Text(L10n.blocked).tag(false) +// } +// .disabled(true) +// } header: { +// Text(L10n.access) +// } footer: { +// LearnMoreButton(L10n.accessTags) { +// TextPair( +// title: L10n.allowed, +// subtitle: L10n.accessTagAllowDescription +// ) +// TextPair( +// title: L10n.blocked, +// subtitle: L10n.accessTagBlockDescription +// ) +// } +// } Section { TextField(L10n.name, text: $tag) .autocorrectionDisabled() + .focused($isTagFocused) } footer: { - if tag.isEmpty || tag == "" { + if tag.isEmpty { Label( L10n.required, systemImage: "exclamationmark.circle.fill" @@ -87,6 +84,9 @@ extension AddServerUserAccessTagsView { } } } + .onFirstAppear { + isTagFocused = true + } } } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/EditServerUserAccessTagsView/Components/EditAccessTagRow.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/EditServerUserAccessTagsView/Components/EditAccessTagRow.swift index de828a251..9f1820a3c 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/EditServerUserAccessTagsView/Components/EditAccessTagRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/EditServerUserAccessTagsView/Components/EditAccessTagRow.swift @@ -14,17 +14,9 @@ extension EditServerUserAccessTagsView { struct EditAccessTagRow: View { - // MARK: - Enviroment Variables - - @Environment(\.isEditing) - private var isEditing - @Environment(\.isSelected) - private var isSelected - // MARK: - Metadata Variables - let item: String - let access: Bool + let tag: String // MARK: - Row Actions @@ -34,36 +26,22 @@ extension EditServerUserAccessTagsView { // MARK: - Body var body: some View { - ListRow {} content: { - rowContent - } - .onSelect(perform: onSelect) - .isSeparatorVisible(false) - .swipeActions { - Button(L10n.delete, systemImage: "trash", action: onDelete) - .tint(.red) - } - } - - // MARK: - Row Content + Button(action: onSelect) { + HStack { + Text(tag) + .frame(maxWidth: .infinity, alignment: .leading) - @ViewBuilder - private var rowContent: some View { - HStack { - VStack(alignment: .leading) { - TextPairView( - leading: item, - trailing: access ? L10n.allowed : L10n.blocked - ) - .foregroundStyle( - isEditing ? (isSelected ? .primary : .secondary) : .primary, .secondary - ) - .font(.headline) + ListRowCheckbox() } - - ListRowCheckbox() - .environment(\.isEditing, isEditing) - .environment(\.isSelected, isSelected) + } + .foregroundStyle(.primary) + .swipeActions { + Button( + L10n.delete, + systemImage: "trash", + action: onDelete + ) + .tint(.red) } } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/EditServerUserAccessTagsView/EditServerUserAccessTagsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/EditServerUserAccessTagsView/EditServerUserAccessTagsView.swift index b052d388e..f42e0007f 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/EditServerUserAccessTagsView/EditServerUserAccessTagsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUserAccessTags/EditServerUserAccessTagsView/EditServerUserAccessTagsView.swift @@ -12,10 +12,10 @@ import SwiftUI struct EditServerUserAccessTagsView: View { - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor + private struct TagWithAccess: Hashable { + let tag: String + let access: Bool + } // MARK: - Observed, State, & Environment Objects @@ -28,14 +28,14 @@ struct EditServerUserAccessTagsView: View { // MARK: - Dialog States @State - private var isPresentingDeleteConfirmation = false + private var isBlockedExpanded: Bool = true @State - private var isPresentingDeleteSelectionConfirmation = false + private var isPresentingDeleteConfirmation = false // MARK: - Editing States @State - private var selectedTags: Set<[String: Bool]> = [] + private var selectedTags: Set = [] @State private var isEditing: Bool = false @@ -44,16 +44,18 @@ struct EditServerUserAccessTagsView: View { @State private var error: Error? - // MARK: - Computed Policy Tags - - private var policyTags: Set<[String: Bool]> { - let blockedTags = viewModel.user.policy?.blockedTags?.map { [$0: false] } ?? [] - // let allowedTags = viewModel.user.policy?.allowedTags?.map { [$0: true] } ?? [] - - // return Set(allowedTags + blockedTags) - return Set(blockedTags) + private var blockedTags: [TagWithAccess] { + viewModel.user.policy?.blockedTags? + .sorted() + .map { TagWithAccess(tag: $0, access: false) } ?? [] } +// private var allowedTags: [TagWithAccess] { +// viewModel.user.policy?.allowedTags? +// .sorted() +// .map { TagWithAccess(tag: $0, access: true) } ?? [] +// } + // MARK: - Initializera init(viewModel: ServerUserAdminViewModel) { @@ -71,7 +73,7 @@ struct EditServerUserAccessTagsView: View { errorView(with: error) } } - .navigationBarTitle(L10n.accessTags) + .navigationTitle(L10n.accessTags) .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(isEditing) .toolbar { @@ -83,20 +85,17 @@ struct EditServerUserAccessTagsView: View { ToolbarItem(placement: .topBarTrailing) { if isEditing { Button(L10n.cancel) { - if isEditing { - isEditing.toggle() - } + isEditing = false UIDevice.impact(.light) selectedTags.removeAll() } .buttonStyle(.toolbarPill) - .foregroundStyle(accentColor) } } ToolbarItem(placement: .bottomBar) { if isEditing { Button(L10n.delete) { - isPresentingDeleteSelectionConfirmation = true + isPresentingDeleteConfirmation = true } .buttonStyle(.toolbarPill(.red)) .disabled(selectedTags.isEmpty) @@ -107,8 +106,7 @@ struct EditServerUserAccessTagsView: View { .navigationBarMenuButton( isLoading: viewModel.backgroundStates.contains(.refreshing), isHidden: isEditing || ( - viewModel.user.policy?.blockedTags == [] && - viewModel.user.policy?.blockedTags == [] + viewModel.user.policy?.blockedTags?.isEmpty == true ) ) { Button(L10n.add, systemImage: "plus") { @@ -121,33 +119,23 @@ struct EditServerUserAccessTagsView: View { } } } - .onReceive(viewModel.events) { events in - switch events { + .onReceive(viewModel.events) { event in + switch event { case let .error(eventError): error = eventError default: break } } - .errorMessage($error) .confirmationDialog( L10n.delete, - isPresented: $isPresentingDeleteSelectionConfirmation, + isPresented: $isPresentingDeleteConfirmation, titleVisibility: .visible ) { deleteSelectedConfirmationActions } message: { Text(L10n.deleteSelectedConfirmation) } - .confirmationDialog( - L10n.delete, - isPresented: $isPresentingDeleteConfirmation, - titleVisibility: .visible - ) { - deleteConfirmationActions - } message: { - Text(L10n.deleteItemConfirmation) - } .errorMessage($error) } @@ -161,8 +149,23 @@ struct EditServerUserAccessTagsView: View { } } + @ViewBuilder + private func makeRow(tag: TagWithAccess) -> some View { + EditAccessTagRow(tag: tag.tag) { + if isEditing { + selectedTags.toggle(value: tag) + } + } onDelete: { + selectedTags = [tag] + isPresentingDeleteConfirmation = true + } + .environment(\.isEditing, isEditing) + .environment(\.isSelected, selectedTags.contains(tag)) + } + // MARK: - Content View + @ViewBuilder private var contentView: some View { List { ListTitleSection( @@ -172,39 +175,23 @@ struct EditServerUserAccessTagsView: View { UIApplication.shared.open(.jellyfinDocsManagingUsers) } - if policyTags.isEmpty { + if blockedTags.isEmpty { Button(L10n.add) { router.route(to: \.userAddAccessTag, viewModel) } } else { - ForEach(policyTags.sorted(by: { - if $0.values.first == $1.values.first { - return $0.keys.first ?? "" < $1.keys.first ?? "" - } else { - return $0.values.first == true - } - }), id: \.self) { tagEntry in - if let tag = tagEntry.keys.first, let access = tagEntry.values.first { - EditAccessTagRow( - item: tag, - access: access - ) { - if isEditing { - if selectedTags.contains(tagEntry) { - selectedTags.remove(tagEntry) - } else { - selectedTags.insert(tagEntry) - } - } - } onDelete: { - selectedTags.removeAll() - selectedTags.insert(tagEntry) - isPresentingDeleteConfirmation = true - } - .environment(\.isEditing, isEditing) - .environment(\.isSelected, selectedTags.contains(tagEntry)) - } + DisclosureGroup( + L10n.blocked, + isExpanded: $isBlockedExpanded + ) { + ForEach( + blockedTags, + id: \.self, + content: makeRow + ) } + + // TODO: allowed with 10.10 } } } @@ -213,13 +200,13 @@ struct EditServerUserAccessTagsView: View { @ViewBuilder private var navigationBarSelectView: some View { - let isAllSelected = selectedTags.count == policyTags.count + let isAllSelected = selectedTags.count == blockedTags.count + Button(isAllSelected ? L10n.removeAll : L10n.selectAll) { - selectedTags = isAllSelected ? [] : policyTags + selectedTags = isAllSelected ? [] : Set(blockedTags) } .buttonStyle(.toolbarPill) .disabled(!isEditing) - .foregroundStyle(accentColor) } // MARK: - Delete Selected Confirmation Actions @@ -228,43 +215,20 @@ struct EditServerUserAccessTagsView: View { private var deleteSelectedConfirmationActions: some View { Button(L10n.cancel, role: .cancel) {} - Button(L10n.confirm, role: .destructive) { + Button(L10n.delete, role: .destructive) { var tempPolicy = viewModel.user.policy ?? UserPolicy() - for tagEntry in selectedTags { - if let tag = tagEntry.keys.first, let isAllowed = tagEntry.values.first { - if isAllowed { - // tempPolicy.allowedTags?.removeAll { $0 == tag } - } else { - tempPolicy.blockedTags?.removeAll { $0 == tag } - } + + for tag in selectedTags { + if tag.access { + // tempPolicy.allowedTags?.removeAll { $0 == tag.tag } + } else { + tempPolicy.blockedTags?.removeAll { $0 == tag.tag } } } + viewModel.send(.updatePolicy(tempPolicy)) selectedTags.removeAll() isEditing = false } } - - // MARK: - Delete Single Confirmation Actions - - @ViewBuilder - private var deleteConfirmationActions: some View { - Button(L10n.cancel, role: .cancel) {} - - Button(L10n.delete, role: .destructive) { - var tempPolicy = viewModel.user.policy ?? UserPolicy() - if let tagEntry = selectedTags.first, selectedTags.count == 1 { - if let tag = tagEntry.keys.first, let isAllowed = tagEntry.values.first { - if isAllowed { - // tempPolicy.allowedTags?.removeAll { $0 == tag } - } else { - tempPolicy.blockedTags?.removeAll { $0 == tag } - } - viewModel.send(.updatePolicy(tempPolicy)) - selectedTags.removeAll() - isEditing = false - } - } - } - } }