mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user