Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,21 @@ import AppKit
// MARK: - Root

struct SieveScriptBuilderView: View {
@State var model: SieveScriptViewModel
@StateObject var model: SieveScriptViewModel
@State private var libraryWidth: CGFloat = 320
@State private var shouldPresentSheet = false
@State private var renderedScriptText: String = ""
@State private var shouldShowClearConfirmation = false

var body: some View {
HSplitView {
ZStack(alignment: .topLeading) {
Color.primary.colorInvert()
.ignoresSafeArea()

Toolbar() {
renderedScriptText = model.render()
shouldPresentSheet = true
}
.ignoresSafeArea()
toolbar

SieveScriptView(viewModel: $model)
SieveScriptView(viewModel: model)
.padding([.leading, .top], 24)
.frame(minWidth: 420,
maxWidth: .infinity,
Expand All @@ -49,30 +46,68 @@ struct SieveScriptBuilderView: View {
} content: {
RenderedSieveScriptView(scriptText: $renderedScriptText)
}
.alert("Clear Script?", isPresented: $shouldShowClearConfirmation) {
Button("Cancel", role: .cancel) { }
Button("Clear", role: .destructive) {
model.clear()
}
} message: {
Text("This will remove all commands from the script. This action cannot be undone.")
}
}

@ViewBuilder
private var toolbar: some View {
Toolbar {
if !model.rowTokens.isEmpty {
ToolbarButton(icon: "trash") {
shouldShowClearConfirmation = true
}
}
ToolbarButton(icon: "note.text") {
renderedScriptText = model.render()
shouldPresentSheet = true
}
}
.ignoresSafeArea()
}
}

private struct Toolbar: View {
var onRender: (() -> Void)
private struct Toolbar<Content: View>: View {
var content: Content

init(@ViewBuilder content: () -> Content) {
self.content = content()
}

var body: some View {
HStack {
HStack(spacing: 32) {
Spacer()
Button {
onRender()
} label: {
Image(systemName: "note.text")
.resizable()
.frame(width: 18, height: 18)
}
.buttonStyle(.plain)

content
}
.padding(.trailing, 24)
.frame(height: 50)
}
}

private struct ToolbarButton: View {
private let size: CGFloat = 22

let icon: String
let action: () -> Void

var body: some View {
Button {
action()
} label: {
Image(systemName: icon)
.resizable()
.frame(width: size, height: size)
}
.buttonStyle(.plain)
}
}

#Preview {
SieveScriptBuilderView(model: SieveScriptViewModel())
.frame(width: 1120, height: 720)
Expand Down
77 changes: 68 additions & 9 deletions Source/Yasba/Views/Sieve Script/SieveScriptView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI
import UniformTypeIdentifiers

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

init(viewModel: SieveScriptViewModel) {
self.viewModel = viewModel
}

var filteredIndices: [Int] {
if let span = draggedRange { return Array(tokens.indices).filter { !span.contains($0) } }
return Array(tokens.indices)
Expand All @@ -29,22 +33,52 @@ struct SieveScriptView: View {
return CGFloat(indents.last ?? 0) * 24
}

init(viewModel: Binding<SieveScriptViewModel>) {
self._viewModel = viewModel
}

var body: some View {
ZStack(alignment: .center) {
HStack {
Spacer()
scriptList
.frame(maxWidth: 1000)
if viewModel.rowTokens.isEmpty {
emptyDropArea
.frame(maxWidth: 1000)
} else {
scriptList
.frame(maxWidth: 1000)
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}

@ViewBuilder
private var emptyDropArea: some View {
VStack(spacing: 12) {
if dropGapIndex != nil || draggedRange != nil {
PlaceholderRowView()
.padding(.leading, 0)
.transition(.opacity)
} else {
Text("Drag a command from the sidebar to start writing your script.")
.font(.title2)
.foregroundStyle(Color.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.contentShape(Rectangle())
.onDrop(
of: [UTType.utf8PlainText],
delegate: UnifiedDropDelegate(
target: .fixedGap(0),
draggedRange: $draggedRange,
dropGapIndex: $dropGapIndex,
rowHeights: $rowHeights,
viewModel: viewModel
)
)
}

@ViewBuilder
var scriptList: some View {
Expand Down Expand Up @@ -151,7 +185,32 @@ struct SieveScriptView: View {
}
}

#Preview {
#Preview("Not empty") {
@Previewable @State var viewModel = SieveScriptViewModel(script: [
AddFlagCommand(tag: "Spam"),
IfCommand(
quantifier: .any,
tests: [
.header(["from"], match: .contains, keys: ["[email protected]"]),
.exists(["x-marked-spam"])
],
thenChildren: [
AddFlagCommand(tag: "Label 1"),
AddFlagCommand(tag: "Label 2")
],
elseChildren: [
AddFlagCommand(tag: "Tag 1")
]
),
FileIntoCommand(mailbox: "Social"),
AddFlagCommand(tag: "Tag 1"),
AddFlagCommand(tag: "Tag 2"),
StopCommand()
])
SieveScriptView(viewModel: viewModel)
}

#Preview("Empty") {
@Previewable @State var viewModel = SieveScriptViewModel()
SieveScriptView(viewModel: $viewModel)
SieveScriptView(viewModel: viewModel)
}
33 changes: 11 additions & 22 deletions Source/Yasba/Views/Sieve Script/SieveScriptViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,7 @@ import Combine

final class SieveScriptViewModel: ObservableObject {

private var script: [AnySieveCommand] = [
AddFlagCommand(tag: "Spam"),
IfCommand(
quantifier: .any,
tests: [
.header(["from"], match: .contains, keys: ["[email protected]"]),
.exists(["x-marked-spam"])
],
thenChildren: [
AddFlagCommand(tag: "Label 1"),
AddFlagCommand(tag: "Label 2")
],
elseChildren: [
AddFlagCommand(tag: "Tag 1")
]
),
FileIntoCommand(mailbox: "Social"),
AddFlagCommand(tag: "Tag 1"),
AddFlagCommand(tag: "Tag 2"),
StopCommand()
]
private var script: [AnySieveCommand] = []

private let mapper: RowTokenMapping
private let editor: RowTokenEditing
Expand All @@ -32,9 +12,11 @@ final class SieveScriptViewModel: ObservableObject {
mapper.tokens(from: script)
}

init(mapper: RowTokenMapping = RowTokenMapper(),
init(script: [AnySieveCommand] = [],
mapper: RowTokenMapping = RowTokenMapper(),
editor: RowTokenEditing = RowTokenEditor(),
scriptRenderer: SieveScriptRenderer = .default) {
self.script = script
self.mapper = mapper
self.editor = editor
self.scriptRenderer = scriptRenderer
Expand Down Expand Up @@ -70,8 +52,15 @@ final class SieveScriptViewModel: ObservableObject {
func render() -> String {
return scriptRenderer.render(commands: script)
}

func clear() {
applyTokenEdit { tokens in
tokens = []
}
}

private func applyTokenEdit(_ transform: (inout [RowToken]) -> Void) {
objectWillChange.send()
var tokens = mapper.tokens(from: script)
transform(&tokens)
self.script = mapper.script(from: tokens)
Expand Down
Loading