Tusker/Tusker/Screens/Compose/ComposePollView.swift

233 lines
7.3 KiB
Swift

//
// ComposePollView.swift
// Tusker
//
// Created by Shadowfacts on 4/28/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import SwiftUI
import TuskerComponents
struct ComposePollView: View {
private static let formatter: DateComponentsFormatter = {
let f = DateComponentsFormatter()
f.maximumUnitCount = 1
f.unitsStyle = .full
f.allowedUnits = [.weekOfMonth, .day, .hour, .minute]
return f
}()
@ObservedObject var draft: OldDraft
@ObservedObject var poll: OldDraft.Poll
@EnvironmentObject var mastodonController: MastodonController
@Environment(\.colorScheme) var colorScheme: ColorScheme
@State private var duration: Duration
init(draft: OldDraft, poll: OldDraft.Poll) {
self.draft = draft
self.poll = poll
self._duration = State(initialValue: .fromTimeInterval(poll.duration) ?? .oneDay)
}
private var canAddOption: Bool {
if let pollConfig = mastodonController.instance?.pollsConfiguration {
return poll.options.count < pollConfig.maxOptions
} else {
return true
}
}
var body: some View {
VStack {
HStack {
Text("Poll")
.font(.headline)
Spacer()
Button(action: self.removePoll) {
Image(systemName: "xmark")
.imageScale(.small)
.padding(4)
}
.accessibilityLabel("Remove poll")
.buttonStyle(.plain)
.accentColor(buttonForegroundColor)
.background(Circle().foregroundColor(buttonBackgroundColor))
.hoverEffect()
}
List {
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
ComposePollOption(poll: poll, option: e.element, optionIndex: e.offset)
.frame(height: 36)
.listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0))
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
.onMove { indices, newIndex in
poll.options.move(fromOffsets: indices, toOffset: newIndex)
}
}
.listStyle(.plain)
.frame(height: 44 * CGFloat(poll.options.count))
Button(action: self.addOption) {
Label {
Text("Add Option")
} icon: {
Image(systemName: "plus")
.foregroundColor(.accentColor)
}
}
.buttonStyle(.borderless)
.disabled(!canAddOption)
HStack {
MenuPicker(selection: $poll.multiple, options: [
.init(value: true, title: "Allow multiple"),
.init(value: false, title: "Single choice"),
])
.frame(maxWidth: .infinity)
MenuPicker(selection: $duration, options: Duration.allCases.map {
.init(value: $0, title: ComposePollView.formatter.string(from: $0.timeInterval)!)
})
.frame(maxWidth: .infinity)
}
}
.padding(8)
.background(
backgroundColor
.cornerRadius(10)
)
.onChange(of: duration, perform: { (value) in
poll.duration = value.timeInterval
})
}
private var backgroundColor: Color {
// in light mode, .secondarySystemBackground has a blue-ish hue, which we don't want
colorScheme == .dark ? Color.appFill : Color(white: 0.95)
}
private var buttonBackgroundColor: Color {
Color(white: colorScheme == .dark ? 0.1 : 0.8)
}
private var buttonForegroundColor: Color {
Color(UIColor.label)
}
private func removePoll() {
withAnimation {
self.draft.poll = nil
}
}
private func addOption() {
poll.options.append(OldDraft.Poll.Option(""))
}
}
extension ComposePollView {
enum Duration: Hashable, Equatable, CaseIterable {
case fiveMinutes, thirtyMinutes, oneHour, sixHours, oneDay, threeDays, sevenDays
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
}
}
}
}
struct ComposePollOption: View {
@ObservedObject var poll: OldDraft.Poll
@ObservedObject var option: OldDraft.Poll.Option
let optionIndex: Int
@EnvironmentObject private var mastodonController: MastodonController
var body: some View {
HStack(spacing: 4) {
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
.animation(.default, value: poll.multiple)
textField
Button(action: self.removeOption) {
Image(systemName: "minus.circle.fill")
}
.buttonStyle(.plain)
.foregroundColor(poll.options.count == 1 ? .gray : .red)
.disabled(poll.options.count == 1)
.hoverEffect()
}
}
private var textField: some View {
var field = ComposeEmojiTextField(text: $option.text, placeholder: "Option \(optionIndex + 1)", maxLength: mastodonController.instance?.pollsConfiguration?.maxCharactersPerOption)
return field.backgroundColor(.appBackground)
}
private func removeOption() {
poll.options.remove(at: optionIndex)
}
struct Checkbox: View {
private let radiusFraction: CGFloat
private let size: CGFloat = 20
private let innerSize: CGFloat
init(radiusFraction: CGFloat, borderWidth: CGFloat) {
self.radiusFraction = radiusFraction
self.innerSize = self.size - 2 * borderWidth
}
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.gray)
.frame(width: size, height: size)
.cornerRadius(radiusFraction * size)
Rectangle()
.foregroundColor(Color(UIColor.appBackground))
.frame(width: innerSize, height: innerSize)
.cornerRadius(radiusFraction * innerSize)
}
}
}
}
//struct ComposePollView_Previews: PreviewProvider {
// static var previews: some View {
// ComposePollView()
// }
//}