style: refine UI layouts, toolbars, and chunk map aesthetics

- Make global and contextual Start/Stop toolbar buttons mutually exclusive in main window
- Relocate Add Downloads primary action buttons from top toolbar to standard bottom Action Bar
- Expand Download Properties window width for better readability and condense font sizes
- Wrap Advanced Transfer settings in a native DisclosureGroup to reduce scrolling
- Redesign Chunk Map with rounded corners, larger boxes, and system accent colors
This commit is contained in:
nimbold
2026-06-04 14:29:13 +03:30
parent 062c4aec61
commit 0040c23050
4 changed files with 69 additions and 84 deletions
+24 -30
View File
@@ -38,9 +38,10 @@ struct AddDownloadsView: View {
}
.padding(12)
}
}
.toolbar {
Divider()
actionBar
.padding(16)
.background(.background)
}
.frame(minWidth: 640, idealWidth: 680, minHeight: 500, idealHeight: 540)
.onChange(of: linkText) { _, newValue in
@@ -218,49 +219,42 @@ struct AddDownloadsView: View {
}
}
@ToolbarContentBuilder
private var actionBar: some ToolbarContent {
ToolbarItem(placement: .status) {
HStack {
Text(actionMessage)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
if metadataTask != nil {
Button {
metadataTask?.cancel()
metadataTask = nil
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
private var actionBar: some View {
HStack {
Text(actionMessage)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
if metadataTask != nil {
Button {
metadataTask?.cancel()
metadataTask = nil
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
}
ToolbarItem(placement: .cancellationAction) {
Spacer()
Button("Cancel") {
dismiss()
}
}
.keyboardShortcut(.cancelAction)
ToolbarItemGroup(placement: .primaryAction) {
Button {
Button("Add to Queue") {
addDownloads(start: false)
} label: {
Label("Add to Queue", systemImage: "list.bullet")
}
.disabled(!canAddDownloads)
Button {
Button("Start Downloads") {
addDownloads(start: true)
} label: {
Label("Start Downloads", systemImage: "play.fill")
}
.buttonStyle(.borderedProminent)
.disabled(!canAddDownloads)
.keyboardShortcut(.defaultAction)
}
}
+10 -9
View File
@@ -114,8 +114,9 @@ struct ChunkGrid: View {
var body: some View {
let itemPieces = pieces
Canvas { context, size in
let boxSize: CGFloat = 6
let spacing: CGFloat = 1
let boxSize: CGFloat = 10
let spacing: CGFloat = 2
let cornerSize = CGSize(width: 2, height: 2)
let width = size.width
let x: CGFloat = 0
@@ -126,10 +127,10 @@ struct ChunkGrid: View {
var cy = y
for piece in itemPieces {
if piece {
p.addRect(CGRect(x: cx, y: cy, width: boxSize - spacing, height: boxSize - spacing))
p.addRoundedRect(in: CGRect(x: cx, y: cy, width: boxSize - spacing, height: boxSize - spacing), cornerSize: cornerSize)
}
cx += boxSize
if cx >= width {
if cx + boxSize > width {
cx = 0
cy += boxSize
}
@@ -141,19 +142,19 @@ struct ChunkGrid: View {
var cy: CGFloat = 0
for piece in itemPieces {
if !piece {
p.addRect(CGRect(x: cx, y: cy, width: boxSize - spacing, height: boxSize - spacing))
p.addRoundedRect(in: CGRect(x: cx, y: cy, width: boxSize - spacing, height: boxSize - spacing), cornerSize: cornerSize)
}
cx += boxSize
if cx >= width {
if cx + boxSize > width {
cx = 0
cy += boxSize
}
}
}
context.fill(pendingPath, with: .color(Color.gray.opacity(0.3)))
context.fill(completedPath, with: .color(Color.green))
context.fill(pendingPath, with: .color(Color.primary.opacity(0.08)))
context.fill(completedPath, with: .color(Color.accentColor))
}
.frame(minHeight: 80)
.frame(minHeight: 140)
}
}
+24 -35
View File
@@ -102,45 +102,34 @@ struct ContentView: View {
}
}
ToolbarItem {
Button {
controller.startQueue(queueID: queueID)
} label: {
Label("Start Queue", systemImage: "play.fill")
}
}
ToolbarItem {
Button {
controller.pauseActiveDownloads(queueID: queueID)
} label: {
Label("Stop Downloads", systemImage: "stop.fill")
}
.disabled(!hasActiveDownloads(in: queueID))
}
ToolbarItemGroup {
if !selectedItems.isEmpty {
if selectedItems.contains(where: { $0.status == .downloading }) {
Button {
for item in selectedItems where item.status == .downloading {
controller.pause(item)
}
} label: {
Label("Stop", systemImage: "stop.fill")
}
}
if selectedItems.contains(where: { $0.status == .paused || $0.status == .failed || $0.status == .canceled }) {
Button {
for item in selectedItems where item.status == .paused || item.status == .failed || item.status == .canceled {
controller.resume(item)
}
} label: {
Label("Start", systemImage: "play.fill")
let canStop = selectedItems.isEmpty ? hasActiveDownloads(in: queueID) : selectedItems.contains(where: { $0.status == .downloading })
Button {
if selectedItems.isEmpty {
controller.pauseActiveDownloads(queueID: queueID)
} else {
for item in selectedItems where item.status == .downloading {
controller.pause(item)
}
}
} label: {
Label(selectedItems.isEmpty ? "Stop All" : "Stop", systemImage: "stop.fill")
}
.disabled(!canStop)
let canStart = selectedItems.isEmpty ? true : selectedItems.contains(where: { $0.status == .paused || $0.status == .failed || $0.status == .canceled })
Button {
if selectedItems.isEmpty {
controller.startQueue(queueID: queueID)
} else {
for item in selectedItems where item.status == .paused || item.status == .failed || item.status == .canceled {
controller.resume(item)
}
}
} label: {
Label(selectedItems.isEmpty ? "Start Queue" : "Start", systemImage: "play.fill")
}
.disabled(!canStart)
}
}
.background {
+11 -10
View File
@@ -46,6 +46,7 @@ struct DownloadPropertiesView: View {
@State private var cookieText: String
@State private var mirrorText: String
@State private var errorMessage = ""
@State private var showsAdvancedTransfer = false
init(item: DownloadItem) {
self.item = item
@@ -83,11 +84,11 @@ struct DownloadPropertiesView: View {
Form {
Section("Download") {
TextField("URL", text: $urlText)
.font(.system(.body, design: .monospaced))
.font(.system(.callout, design: .monospaced))
TextField("File name", text: $fileName)
HStack {
TextField("Save location", text: $destinationPath)
.font(.system(.body, design: .monospaced))
.font(.system(.callout, design: .monospaced))
Button {
selectDestination()
} label: {
@@ -124,7 +125,7 @@ struct DownloadPropertiesView: View {
}
}
Section("Advanced Transfer") {
DisclosureGroup("Advanced Transfer", isExpanded: $showsAdvancedTransfer) {
Toggle("Checksum", isOn: $checksumEnabled)
if checksumEnabled {
Picker("Algorithm", selection: $checksumAlgorithm) {
@@ -133,24 +134,24 @@ struct DownloadPropertiesView: View {
}
}
TextField("Expected digest", text: $checksumValue)
.font(.system(.body, design: .monospaced))
.font(.system(.callout, design: .monospaced))
}
VStack(alignment: .leading, spacing: 6) {
Text("Headers")
TextEditor(text: $headerText)
.font(.system(.body, design: .monospaced))
.frame(minHeight: 70)
.font(.system(.callout, design: .monospaced))
.frame(minHeight: 60)
}
TextField("Cookies", text: $cookieText)
.font(.system(.body, design: .monospaced))
.font(.system(.callout, design: .monospaced))
VStack(alignment: .leading, spacing: 6) {
Text("Mirrors")
TextEditor(text: $mirrorText)
.font(.system(.body, design: .monospaced))
.frame(minHeight: 70)
.font(.system(.callout, design: .monospaced))
.frame(minHeight: 60)
}
}
@@ -187,7 +188,7 @@ struct DownloadPropertiesView: View {
.padding(14)
.background(.bar)
}
.frame(width: 620, height: 760)
.frame(width: 720, height: 720)
}
private var matchingLoginText: String {