Skip to content

Commit 969db64

Browse files
authored
Handle empty state and resetting the script (#2)
* Handle empty script view state * Add button to clear the model * Show alert before clearing the script * Only show clear when script non empty
1 parent 02da087 commit 969db64

File tree

3 files changed

+133
-50
lines changed

3 files changed

+133
-50
lines changed

Source/Yasba/Views/Sieve Script Builder/SieveScriptBuilderView.swift

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,21 @@ import AppKit
77
// MARK: - Root
88

99
struct SieveScriptBuilderView: View {
10-
@State var model: SieveScriptViewModel
10+
@StateObject var model: SieveScriptViewModel
1111
@State private var libraryWidth: CGFloat = 320
1212
@State private var shouldPresentSheet = false
1313
@State private var renderedScriptText: String = ""
14+
@State private var shouldShowClearConfirmation = false
1415

1516
var body: some View {
1617
HSplitView {
1718
ZStack(alignment: .topLeading) {
1819
Color.primary.colorInvert()
1920
.ignoresSafeArea()
2021

21-
Toolbar() {
22-
renderedScriptText = model.render()
23-
shouldPresentSheet = true
24-
}
25-
.ignoresSafeArea()
22+
toolbar
2623

27-
SieveScriptView(viewModel: $model)
24+
SieveScriptView(viewModel: model)
2825
.padding([.leading, .top], 24)
2926
.frame(minWidth: 420,
3027
maxWidth: .infinity,
@@ -49,30 +46,68 @@ struct SieveScriptBuilderView: View {
4946
} content: {
5047
RenderedSieveScriptView(scriptText: $renderedScriptText)
5148
}
49+
.alert("Clear Script?", isPresented: $shouldShowClearConfirmation) {
50+
Button("Cancel", role: .cancel) { }
51+
Button("Clear", role: .destructive) {
52+
model.clear()
53+
}
54+
} message: {
55+
Text("This will remove all commands from the script. This action cannot be undone.")
56+
}
57+
}
58+
59+
@ViewBuilder
60+
private var toolbar: some View {
61+
Toolbar {
62+
if !model.rowTokens.isEmpty {
63+
ToolbarButton(icon: "trash") {
64+
shouldShowClearConfirmation = true
65+
}
66+
}
67+
ToolbarButton(icon: "note.text") {
68+
renderedScriptText = model.render()
69+
shouldPresentSheet = true
70+
}
71+
}
72+
.ignoresSafeArea()
5273
}
5374
}
5475

55-
private struct Toolbar: View {
56-
var onRender: (() -> Void)
76+
private struct Toolbar<Content: View>: View {
77+
var content: Content
78+
79+
init(@ViewBuilder content: () -> Content) {
80+
self.content = content()
81+
}
5782

5883
var body: some View {
59-
HStack {
84+
HStack(spacing: 32) {
6085
Spacer()
61-
Button {
62-
onRender()
63-
} label: {
64-
Image(systemName: "note.text")
65-
.resizable()
66-
.frame(width: 18, height: 18)
67-
}
68-
.buttonStyle(.plain)
69-
86+
content
7087
}
7188
.padding(.trailing, 24)
7289
.frame(height: 50)
7390
}
7491
}
7592

93+
private struct ToolbarButton: View {
94+
private let size: CGFloat = 22
95+
96+
let icon: String
97+
let action: () -> Void
98+
99+
var body: some View {
100+
Button {
101+
action()
102+
} label: {
103+
Image(systemName: icon)
104+
.resizable()
105+
.frame(width: size, height: size)
106+
}
107+
.buttonStyle(.plain)
108+
}
109+
}
110+
76111
#Preview {
77112
SieveScriptBuilderView(model: SieveScriptViewModel())
78113
.frame(width: 1120, height: 720)

Source/Yasba/Views/Sieve Script/SieveScriptView.swift

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import SwiftUI
22
import UniformTypeIdentifiers
33

44
struct SieveScriptView: View {
5-
@Binding private var viewModel: SieveScriptViewModel
5+
@ObservedObject private var viewModel: SieveScriptViewModel
66
@State private var draggedRange: Range<Int>? = nil
77
@State private var dropGapIndex: Int? = nil
88
@State private var rowHeights: [Int: CGFloat] = [:]
@@ -12,6 +12,10 @@ struct SieveScriptView: View {
1212
var tokens: [RowToken] { viewModel.rowTokens }
1313
var indents: [Int] { viewModel.indentation(for: tokens) }
1414

15+
init(viewModel: SieveScriptViewModel) {
16+
self.viewModel = viewModel
17+
}
18+
1519
var filteredIndices: [Int] {
1620
if let span = draggedRange { return Array(tokens.indices).filter { !span.contains($0) } }
1721
return Array(tokens.indices)
@@ -29,22 +33,52 @@ struct SieveScriptView: View {
2933
return CGFloat(indents.last ?? 0) * 24
3034
}
3135

32-
init(viewModel: Binding<SieveScriptViewModel>) {
33-
self._viewModel = viewModel
34-
}
35-
3636
var body: some View {
3737
ZStack(alignment: .center) {
3838
HStack {
3939
Spacer()
40-
scriptList
41-
.frame(maxWidth: 1000)
40+
if viewModel.rowTokens.isEmpty {
41+
emptyDropArea
42+
.frame(maxWidth: 1000)
43+
} else {
44+
scriptList
45+
.frame(maxWidth: 1000)
46+
}
4247
Spacer()
4348
}
4449
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
4550
}
4651
.frame(maxWidth: .infinity, maxHeight: .infinity)
4752
}
53+
54+
@ViewBuilder
55+
private var emptyDropArea: some View {
56+
VStack(spacing: 12) {
57+
if dropGapIndex != nil || draggedRange != nil {
58+
PlaceholderRowView()
59+
.padding(.leading, 0)
60+
.transition(.opacity)
61+
} else {
62+
Text("Drag a command from the sidebar to start writing your script.")
63+
.font(.title2)
64+
.foregroundStyle(Color.secondary)
65+
.multilineTextAlignment(.center)
66+
.padding(.horizontal, 24)
67+
}
68+
}
69+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
70+
.contentShape(Rectangle())
71+
.onDrop(
72+
of: [UTType.utf8PlainText],
73+
delegate: UnifiedDropDelegate(
74+
target: .fixedGap(0),
75+
draggedRange: $draggedRange,
76+
dropGapIndex: $dropGapIndex,
77+
rowHeights: $rowHeights,
78+
viewModel: viewModel
79+
)
80+
)
81+
}
4882

4983
@ViewBuilder
5084
var scriptList: some View {
@@ -151,7 +185,32 @@ struct SieveScriptView: View {
151185
}
152186
}
153187

154-
#Preview {
188+
#Preview("Not empty") {
189+
@Previewable @State var viewModel = SieveScriptViewModel(script: [
190+
AddFlagCommand(tag: "Spam"),
191+
IfCommand(
192+
quantifier: .any,
193+
tests: [
194+
.header(["from"], match: .contains, keys: ["[email protected]"]),
195+
.exists(["x-marked-spam"])
196+
],
197+
thenChildren: [
198+
AddFlagCommand(tag: "Label 1"),
199+
AddFlagCommand(tag: "Label 2")
200+
],
201+
elseChildren: [
202+
AddFlagCommand(tag: "Tag 1")
203+
]
204+
),
205+
FileIntoCommand(mailbox: "Social"),
206+
AddFlagCommand(tag: "Tag 1"),
207+
AddFlagCommand(tag: "Tag 2"),
208+
StopCommand()
209+
])
210+
SieveScriptView(viewModel: viewModel)
211+
}
212+
213+
#Preview("Empty") {
155214
@Previewable @State var viewModel = SieveScriptViewModel()
156-
SieveScriptView(viewModel: $viewModel)
215+
SieveScriptView(viewModel: viewModel)
157216
}

Source/Yasba/Views/Sieve Script/SieveScriptViewModel.swift

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,7 @@ import Combine
22

33
final class SieveScriptViewModel: ObservableObject {
44

5-
private var script: [AnySieveCommand] = [
6-
AddFlagCommand(tag: "Spam"),
7-
IfCommand(
8-
quantifier: .any,
9-
tests: [
10-
.header(["from"], match: .contains, keys: ["[email protected]"]),
11-
.exists(["x-marked-spam"])
12-
],
13-
thenChildren: [
14-
AddFlagCommand(tag: "Label 1"),
15-
AddFlagCommand(tag: "Label 2")
16-
],
17-
elseChildren: [
18-
AddFlagCommand(tag: "Tag 1")
19-
]
20-
),
21-
FileIntoCommand(mailbox: "Social"),
22-
AddFlagCommand(tag: "Tag 1"),
23-
AddFlagCommand(tag: "Tag 2"),
24-
StopCommand()
25-
]
5+
private var script: [AnySieveCommand] = []
266

277
private let mapper: RowTokenMapping
288
private let editor: RowTokenEditing
@@ -32,9 +12,11 @@ final class SieveScriptViewModel: ObservableObject {
3212
mapper.tokens(from: script)
3313
}
3414

35-
init(mapper: RowTokenMapping = RowTokenMapper(),
15+
init(script: [AnySieveCommand] = [],
16+
mapper: RowTokenMapping = RowTokenMapper(),
3617
editor: RowTokenEditing = RowTokenEditor(),
3718
scriptRenderer: SieveScriptRenderer = .default) {
19+
self.script = script
3820
self.mapper = mapper
3921
self.editor = editor
4022
self.scriptRenderer = scriptRenderer
@@ -70,8 +52,15 @@ final class SieveScriptViewModel: ObservableObject {
7052
func render() -> String {
7153
return scriptRenderer.render(commands: script)
7254
}
55+
56+
func clear() {
57+
applyTokenEdit { tokens in
58+
tokens = []
59+
}
60+
}
7361

7462
private func applyTokenEdit(_ transform: (inout [RowToken]) -> Void) {
63+
objectWillChange.send()
7564
var tokens = mapper.tokens(from: script)
7665
transform(&tokens)
7766
self.script = mapper.script(from: tokens)

0 commit comments

Comments
 (0)