// // EditFilterView.swift // Tusker // // Created by Shadowfacts on 12/2/22. // Copyright © 2022 Shadowfacts. All rights reserved. // import SwiftUI import Pachyderm import TuskerComponents struct EditFilterView: View { private static let expiresInOptions: [MenuPicker.Option] = { let f = DateComponentsFormatter() f.maximumUnitCount = 1 f.unitsStyle = .full f.allowedUnits = [.weekOfMonth, .day, .hour, .minute] let durations: [TimeInterval] = [ 30 * 60, 60 * 60, 6 * 60 * 60, 24 * 60 * 60, 3 * 24 * 60 * 60, 7 * 24 * 60 * 60, ] return durations.map { .init(value: $0, title: f.string(from: $0)!) } }() @ObservedObject private var filter: EditedFilter private let create: Bool private let originallyExpired: Bool @EnvironmentObject private var mastodonController: MastodonController @Environment(\.dismiss) private var dismiss @State private var expiresIn: TimeInterval @State private var originalFilter: EditedFilter @State private var edited = false @State private var isSaving = false @State private var saveError: (any Error)? init(filter: EditedFilter, create: Bool, originallyExpired: Bool) { self.filter = filter self.create = create self.originallyExpired = originallyExpired self._originalFilter = State(wrappedValue: EditedFilter(copying: filter)) if let expiresIn = filter.expiresIn { self._expiresIn = State(wrappedValue: Self.expiresInOptions.min(by: { a, b in let aDist = abs(a.value - expiresIn) let bDist = abs(b.value - expiresIn) return aDist < bDist })!.value) } else { self._expiresIn = State(wrappedValue: EditedFilter.defaultExpiresInForExpired) } } private var expires: Binding { Binding { filter.expiresIn != nil } set: { newValue in filter.expiresIn = newValue ? expiresIn : nil } } var body: some View { Form { if mastodonController.instanceFeatures.filtersV2 { Section { TextField("Title", text: Binding(get: { filter.title ?? "" }, set: { newValue in filter.title = newValue })) } .appGroupedListRowBackground() } Section { ForEach(Array($filter.keywords.enumerated()), id: \.offset) { keyword in VStack { TextField("Phrase", text: keyword.element.keyword) Toggle("Whole Word", isOn: keyword.element.wholeWord) } } .onDelete(perform: mastodonController.instanceFeatures.filtersV2 ? { indices in filter.keywords.remove(atOffsets: indices) } : nil) if mastodonController.instanceFeatures.filtersV2 { Button { let new = EditedFilter.Keyword(id: nil, keyword: "", wholeWord: true) withAnimation { filter.keywords.append(new) } } label: { Label("Add Keyword", systemImage: "plus") } } } .appGroupedListRowBackground() Section { if mastodonController.instanceFeatures.filtersV2 { Picker(selection: $filter.action) { ForEach(FilterV2.Action.allCases, id: \.self) { action in Text(action.displayName).tag(action) } } label: { Text("Action") } } Toggle("Expires", isOn: expires) if expires.wrappedValue { Picker(selection: $expiresIn) { ForEach(Self.expiresInOptions, id: \.value) { option in Text(option.title).tag(option.value) } } label: { Text("Duration") } } } .appGroupedListRowBackground() Section { ForEach(FilterV1.Context.allCases, id: \.rawValue) { context in Toggle(isOn: Binding(get: { filter.contexts.contains(context) }, set: { newValue in if newValue { if !filter.contexts.contains(context) { filter.contexts.append(context) } } else if filter.contexts.count > 1 { filter.contexts.removeAll(where: { $0 == context }) } })) { Text(context.displayName) } .toggleStyle(FilterContextToggleStyle()) } } header: { Text("Contexts") } .appGroupedListRowBackground() } .appGroupedListBackground(container: UIHostingController.self) #if !os(visionOS) .scrollDismissesKeyboardInteractivelyIfAvailable() #endif .navigationTitle(create ? "Add Filter" : "Edit Filter") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { if isSaving { ProgressView() .progressViewStyle(.circular) } else { Button("Save") { saveFilter() } .disabled(!filter.isValid(for: mastodonController) || (!edited && originallyExpired)) } } } .alertWithData("Error Saving Filter", data: $saveError, actions: { _ in Button("OK") {} }, message: { error in Text(error.localizedDescription) }) #if os(visionOS) .onChange(of: expiresIn) { edited = true if expires.wrappedValue { filter.expiresIn = expiresIn } } #else .onChange(of: expiresIn, perform: { newValue in edited = true if expires.wrappedValue { filter.expiresIn = newValue } }) #endif .onReceive(filter.objectWillChange, perform: { _ in edited = true }) } private func saveFilter() { Task { do { isSaving = true if create { try await CreateFilterService(filter: filter, mastodonController: mastodonController).run() } else { try await UpdateFilterService(filter: filter, mastodonController: mastodonController).run() } dismiss() } catch { isSaving = false saveError = error } } } } private struct FilterContextToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) -> some View { Button { configuration.isOn.toggle() } label: { HStack { configuration.label .foregroundColor(.primary) Spacer() if configuration.isOn { Image(systemName: "checkmark") } } } } } private extension View { @available(iOS, obsoleted: 16.0) @ViewBuilder func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View { if #available(iOS 16.0, *) { self.scrollDismissesKeyboard(.interactively) } else { self } } } //struct EditFilterView_Previews: PreviewProvider { // static var previews: some View { // EditFilterView() // } //}