// // PollController.swift // ComposeUI // // Created by Shadowfacts on 3/25/23. // import SwiftUI import TuskerComponents class PollController: ViewController { unowned let parent: ComposeController var draft: Draft { parent.draft } let poll: Draft.Poll @Published var duration: Duration init(parent: ComposeController, poll: Draft.Poll) { self.parent = parent self.poll = poll self.duration = .fromTimeInterval(poll.duration) ?? .oneDay } var view: some View { PollView() .environmentObject(poll) } private func removePoll() { withAnimation { draft.poll = nil } } private func moveOptions(indices: IndexSet, newIndex: Int) { poll.options.move(fromOffsets: indices, toOffset: newIndex) } private func removeOption(_ option: Draft.Poll.Option) { poll.options.removeAll(where: { $0.id == option.id }) } private var canAddOption: Bool { if let max = parent.mastodonController.instanceFeatures.maxPollOptionsCount { return poll.options.count < max } else { return true } } private func addOption() { poll.options.append(.init("")) } struct PollView: View { @EnvironmentObject private var controller: PollController @EnvironmentObject private var poll: Draft.Poll @Environment(\.colorScheme) private var colorScheme var body: some View { VStack { HStack { Text("Poll") .font(.headline) Spacer() Button(action: controller.removePoll) { Image(systemName: "xmark") .imageScale(.small) .padding(4) } .accessibilityLabel("Remove poll") .buttonStyle(.plain) .accentColor(buttonForegroundColor) .background(Circle().foregroundColor(buttonBackgroundColor)) .hoverEffect() } List { ForEach(poll.options) { option in PollOptionView(option: option, remove: { controller.removeOption(option) }) .frame(height: 36) .listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0)) .listRowSeparator(.hidden) .listRowBackground(Color.clear) } .onMove(perform: controller.moveOptions) } .listStyle(.plain) .scrollDisabledIfAvailable(true) .frame(height: 44 * CGFloat(poll.options.count)) Button(action: controller.addOption) { Label { Text("Add Option") } icon: { Image(systemName: "plus") .foregroundColor(.accentColor) } } .buttonStyle(.borderless) .disabled(!controller.canAddOption) HStack { MenuPicker(selection: $poll.multiple, options: [ .init(value: true, title: "Allow multiple"), .init(value: false, title: "Single choice"), ]) .frame(maxWidth: .infinity) MenuPicker(selection: $controller.duration, options: Duration.allCases.map { .init(value: $0, title: Duration.formatter.string(from: $0.timeInterval)!) }) .frame(maxWidth: .infinity) } } .padding(8) .background( backgroundColor .cornerRadius(10) ) .onChange(of: controller.duration) { newValue in poll.duration = newValue.timeInterval } } private var backgroundColor: Color { // in light mode, .secondarySystemBackground has a blue-ish hue, which we don't want colorScheme == .dark ? controller.parent.config.fillColor : Color(white: 0.95) } private var buttonForegroundColor: Color { Color(uiColor: .label) } private var buttonBackgroundColor: Color { Color(white: colorScheme == .dark ? 0.1 : 0.8) } } } extension PollController { enum Duration: Hashable, Equatable, CaseIterable { case fiveMinutes, thirtyMinutes, oneHour, sixHours, oneDay, threeDays, sevenDays static let formatter: DateComponentsFormatter = { let f = DateComponentsFormatter() f.maximumUnitCount = 1 f.unitsStyle = .full f.allowedUnits = [.weekOfMonth, .day, .hour, .minute] return f }() static func fromTimeInterval(_ ti: TimeInterval) -> Duration? { for it in allCases where it.timeInterval == ti { return it } return nil } var timeInterval: TimeInterval { switch self { case .fiveMinutes: return 5 * 60 case .thirtyMinutes: return 30 * 60 case .oneHour: return 60 * 60 case .sixHours: return 6 * 60 * 60 case .oneDay: return 24 * 60 * 60 case .threeDays: return 3 * 24 * 60 * 60 case .sevenDays: return 7 * 24 * 60 * 60 } } } }