Tusker/Tusker/Screens/Customize Timelines/EditFilterView.swift

246 lines
8.2 KiB
Swift

//
// 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<TimeInterval>.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<Bool> {
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<CustomizeTimelinesList>.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()
// }
//}