mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-28 19:47:26 +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)
|
.padding(12)
|
||||||
}
|
}
|
||||||
}
|
Divider()
|
||||||
.toolbar {
|
|
||||||
actionBar
|
actionBar
|
||||||
|
.padding(16)
|
||||||
|
.background(.background)
|
||||||
}
|
}
|
||||||
.frame(minWidth: 640, idealWidth: 680, minHeight: 500, idealHeight: 540)
|
.frame(minWidth: 640, idealWidth: 680, minHeight: 500, idealHeight: 540)
|
||||||
.onChange(of: linkText) { _, newValue in
|
.onChange(of: linkText) { _, newValue in
|
||||||
@@ -218,49 +219,42 @@ struct AddDownloadsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ToolbarContentBuilder
|
private var actionBar: some View {
|
||||||
private var actionBar: some ToolbarContent {
|
HStack {
|
||||||
ToolbarItem(placement: .status) {
|
Text(actionMessage)
|
||||||
HStack {
|
.font(.caption)
|
||||||
Text(actionMessage)
|
.foregroundStyle(.secondary)
|
||||||
.font(.caption)
|
.lineLimit(1)
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.lineLimit(1)
|
if metadataTask != nil {
|
||||||
|
Button {
|
||||||
if metadataTask != nil {
|
metadataTask?.cancel()
|
||||||
Button {
|
metadataTask = nil
|
||||||
metadataTask?.cancel()
|
} label: {
|
||||||
metadataTask = nil
|
Image(systemName: "xmark.circle.fill")
|
||||||
} label: {
|
.foregroundStyle(.secondary)
|
||||||
Image(systemName: "xmark.circle.fill")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
Spacer()
|
||||||
|
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
.keyboardShortcut(.cancelAction)
|
||||||
|
|
||||||
ToolbarItemGroup(placement: .primaryAction) {
|
Button("Add to Queue") {
|
||||||
Button {
|
|
||||||
addDownloads(start: false)
|
addDownloads(start: false)
|
||||||
} label: {
|
|
||||||
Label("Add to Queue", systemImage: "list.bullet")
|
|
||||||
}
|
}
|
||||||
.disabled(!canAddDownloads)
|
.disabled(!canAddDownloads)
|
||||||
|
|
||||||
Button {
|
Button("Start Downloads") {
|
||||||
addDownloads(start: true)
|
addDownloads(start: true)
|
||||||
} label: {
|
|
||||||
Label("Start Downloads", systemImage: "play.fill")
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.disabled(!canAddDownloads)
|
.disabled(!canAddDownloads)
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,8 +114,9 @@ struct ChunkGrid: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
let itemPieces = pieces
|
let itemPieces = pieces
|
||||||
Canvas { context, size in
|
Canvas { context, size in
|
||||||
let boxSize: CGFloat = 6
|
let boxSize: CGFloat = 10
|
||||||
let spacing: CGFloat = 1
|
let spacing: CGFloat = 2
|
||||||
|
let cornerSize = CGSize(width: 2, height: 2)
|
||||||
let width = size.width
|
let width = size.width
|
||||||
|
|
||||||
let x: CGFloat = 0
|
let x: CGFloat = 0
|
||||||
@@ -126,10 +127,10 @@ struct ChunkGrid: View {
|
|||||||
var cy = y
|
var cy = y
|
||||||
for piece in itemPieces {
|
for piece in itemPieces {
|
||||||
if piece {
|
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
|
cx += boxSize
|
||||||
if cx >= width {
|
if cx + boxSize > width {
|
||||||
cx = 0
|
cx = 0
|
||||||
cy += boxSize
|
cy += boxSize
|
||||||
}
|
}
|
||||||
@@ -141,19 +142,19 @@ struct ChunkGrid: View {
|
|||||||
var cy: CGFloat = 0
|
var cy: CGFloat = 0
|
||||||
for piece in itemPieces {
|
for piece in itemPieces {
|
||||||
if !piece {
|
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
|
cx += boxSize
|
||||||
if cx >= width {
|
if cx + boxSize > width {
|
||||||
cx = 0
|
cx = 0
|
||||||
cy += boxSize
|
cy += boxSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context.fill(pendingPath, with: .color(Color.gray.opacity(0.3)))
|
context.fill(pendingPath, with: .color(Color.primary.opacity(0.08)))
|
||||||
context.fill(completedPath, with: .color(Color.green))
|
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 {
|
ToolbarItemGroup {
|
||||||
if !selectedItems.isEmpty {
|
let canStop = selectedItems.isEmpty ? hasActiveDownloads(in: queueID) : selectedItems.contains(where: { $0.status == .downloading })
|
||||||
if selectedItems.contains(where: { $0.status == .downloading }) {
|
Button {
|
||||||
Button {
|
if selectedItems.isEmpty {
|
||||||
for item in selectedItems where item.status == .downloading {
|
controller.pauseActiveDownloads(queueID: queueID)
|
||||||
controller.pause(item)
|
} else {
|
||||||
}
|
for item in selectedItems where item.status == .downloading {
|
||||||
} label: {
|
controller.pause(item)
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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 {
|
.background {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ struct DownloadPropertiesView: View {
|
|||||||
@State private var cookieText: String
|
@State private var cookieText: String
|
||||||
@State private var mirrorText: String
|
@State private var mirrorText: String
|
||||||
@State private var errorMessage = ""
|
@State private var errorMessage = ""
|
||||||
|
@State private var showsAdvancedTransfer = false
|
||||||
|
|
||||||
init(item: DownloadItem) {
|
init(item: DownloadItem) {
|
||||||
self.item = item
|
self.item = item
|
||||||
@@ -83,11 +84,11 @@ struct DownloadPropertiesView: View {
|
|||||||
Form {
|
Form {
|
||||||
Section("Download") {
|
Section("Download") {
|
||||||
TextField("URL", text: $urlText)
|
TextField("URL", text: $urlText)
|
||||||
.font(.system(.body, design: .monospaced))
|
.font(.system(.callout, design: .monospaced))
|
||||||
TextField("File name", text: $fileName)
|
TextField("File name", text: $fileName)
|
||||||
HStack {
|
HStack {
|
||||||
TextField("Save location", text: $destinationPath)
|
TextField("Save location", text: $destinationPath)
|
||||||
.font(.system(.body, design: .monospaced))
|
.font(.system(.callout, design: .monospaced))
|
||||||
Button {
|
Button {
|
||||||
selectDestination()
|
selectDestination()
|
||||||
} label: {
|
} label: {
|
||||||
@@ -124,7 +125,7 @@ struct DownloadPropertiesView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Advanced Transfer") {
|
DisclosureGroup("Advanced Transfer", isExpanded: $showsAdvancedTransfer) {
|
||||||
Toggle("Checksum", isOn: $checksumEnabled)
|
Toggle("Checksum", isOn: $checksumEnabled)
|
||||||
if checksumEnabled {
|
if checksumEnabled {
|
||||||
Picker("Algorithm", selection: $checksumAlgorithm) {
|
Picker("Algorithm", selection: $checksumAlgorithm) {
|
||||||
@@ -133,24 +134,24 @@ struct DownloadPropertiesView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextField("Expected digest", text: $checksumValue)
|
TextField("Expected digest", text: $checksumValue)
|
||||||
.font(.system(.body, design: .monospaced))
|
.font(.system(.callout, design: .monospaced))
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text("Headers")
|
Text("Headers")
|
||||||
TextEditor(text: $headerText)
|
TextEditor(text: $headerText)
|
||||||
.font(.system(.body, design: .monospaced))
|
.font(.system(.callout, design: .monospaced))
|
||||||
.frame(minHeight: 70)
|
.frame(minHeight: 60)
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField("Cookies", text: $cookieText)
|
TextField("Cookies", text: $cookieText)
|
||||||
.font(.system(.body, design: .monospaced))
|
.font(.system(.callout, design: .monospaced))
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text("Mirrors")
|
Text("Mirrors")
|
||||||
TextEditor(text: $mirrorText)
|
TextEditor(text: $mirrorText)
|
||||||
.font(.system(.body, design: .monospaced))
|
.font(.system(.callout, design: .monospaced))
|
||||||
.frame(minHeight: 70)
|
.frame(minHeight: 60)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +188,7 @@ struct DownloadPropertiesView: View {
|
|||||||
.padding(14)
|
.padding(14)
|
||||||
.background(.bar)
|
.background(.bar)
|
||||||
}
|
}
|
||||||
.frame(width: 620, height: 760)
|
.frame(width: 720, height: 720)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var matchingLoginText: String {
|
private var matchingLoginText: String {
|
||||||
|
|||||||
Reference in New Issue
Block a user