fix: keychain prompts, AddDownloadsView dynamic sizing, and options format

This commit is contained in:
NimBold
2026-06-10 01:38:55 +03:30
parent 17dc080a20
commit 62297f61ac
2 changed files with 124 additions and 152 deletions
+122 -150
View File
@@ -32,25 +32,37 @@ struct AddDownloadsView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
ScrollView { HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 12) { // Left Column
linkSection VStack(spacing: 0) {
VStack(alignment: .leading, spacing: 16) {
linkSection
optionsSection summarySection
advancedTransferSection previewSection
}
summarySection .padding(20)
previewSection
} }
.padding(12) .frame(maxWidth: .infinity, maxHeight: .infinity)
Divider()
// Right Column
VStack(spacing: 0) {
Form {
optionsSection
advancedTransferSection
}
.formStyle(.grouped)
}
.frame(width: 320)
.background(Color(NSColor.controlBackgroundColor))
} }
Divider() Divider()
actionBar actionBar
.padding(16) .padding(16)
.background(.background) .background(.background)
} }
.frame(minWidth: 640, idealWidth: 680, minHeight: 620, idealHeight: 680) .frame(minWidth: 720, idealWidth: 800, minHeight: 620, idealHeight: 680)
.sheet(isPresented: $showingDuplicates) { .sheet(isPresented: $showingDuplicates) {
DuplicateResolutionView( DuplicateResolutionView(
conflicts: $conflictingDownloads, conflicts: $conflictingDownloads,
@@ -107,7 +119,7 @@ struct AddDownloadsView: View {
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(.quaternary.opacity(0.35)) .background(.quaternary.opacity(0.35))
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
.frame(height: 72) .frame(minHeight: 72, idealHeight: 100, maxHeight: 160)
HStack { HStack {
Text("\(pendingDownloads.count) valid link\(pendingDownloads.count == 1 ? "" : "s") detected") Text("\(pendingDownloads.count) valid link\(pendingDownloads.count == 1 ? "" : "s") detected")
@@ -125,102 +137,9 @@ struct AddDownloadsView: View {
} }
private var optionsSection: some View { private var optionsSection: some View {
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 8) { Group {
GridRow {
Label("Save Location", systemImage: "folder")
.font(.subheadline.weight(.semibold))
Toggle("Use one folder for all files", isOn: $overrideDestination)
}
GridRow {
Text("")
HStack(spacing: 8) {
TextField("Automatic by file type", text: $destinationPath)
.textFieldStyle(.roundedBorder)
.font(.system(.callout, design: .monospaced))
.disabled(!overrideDestination)
Button {
selectDestination()
} label: {
Label("Select...", systemImage: "folder.badge.plus")
}
.disabled(!overrideDestination)
}
}
GridRow(alignment: .firstTextBaseline) {
Label("Queue", systemImage: "tray.full")
.font(.subheadline.weight(.semibold))
Picker("Queue", selection: $targetQueueID) {
ForEach(controller.queues) { queue in
Text(queue.name).tag(queue.id)
}
}
.labelsHidden()
.frame(maxWidth: 220, alignment: .leading)
}
GridRow(alignment: .firstTextBaseline) {
Label("Connections per File", systemImage: "point.3.connected.trianglepath.dotted")
.font(.subheadline.weight(.semibold))
VStack(alignment: .leading, spacing: 4) {
HStack {
Slider(value: $connectionsPerServer, in: 1...16, step: 1)
.frame(width: 145)
Text("\(Int(connectionsPerServer)) segments")
.monospacedDigit()
.frame(width: 98, alignment: .leading)
}
}
}
GridRow(alignment: .firstTextBaseline) {
Label("Speed Limit per File", systemImage: "speedometer")
.font(.subheadline.weight(.semibold))
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
Toggle("Limit each file", isOn: $speedLimitEnabled)
.toggleStyle(.switch)
Stepper(
"\(speedLimitKiBPerSecond) KiB/s",
value: $speedLimitKiBPerSecond,
in: 1...10_485_760,
step: 128
)
.disabled(!speedLimitEnabled)
}
}
}
GridRow(alignment: .top) {
Label("Authorization", systemImage: "lock.shield")
.font(.subheadline.weight(.semibold))
.padding(.top, 4)
VStack(alignment: .leading, spacing: 8) {
Toggle("Use authorization", isOn: $useAuthorization)
.toggleStyle(.switch)
if useAuthorization {
HStack(spacing: 8) {
TextField("Username", text: $authUsername)
.textFieldStyle(.roundedBorder)
.frame(width: 145)
SecureField("Password", text: $authPassword)
.textFieldStyle(.roundedBorder)
.frame(width: 145)
}
Toggle("Save login for this website", isOn: $saveLogin)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
if let firstMedia = pendingDownloads.first(where: { $0.isMedia }), !firstMedia.mediaOptions.isEmpty { if let firstMedia = pendingDownloads.first(where: { $0.isMedia }), !firstMedia.mediaOptions.isEmpty {
GridRow(alignment: .firstTextBaseline) { Section {
Label("Media Format", systemImage: "film")
.font(.subheadline.weight(.semibold))
Picker("Format", selection: Binding( Picker("Format", selection: Binding(
get: { firstMedia.selectedMediaOption?.id ?? "" }, get: { firstMedia.selectedMediaOption?.id ?? "" },
set: { newId in set: { newId in
@@ -241,7 +160,68 @@ struct AddDownloadsView: View {
} }
} }
.labelsHidden() .labelsHidden()
.frame(maxWidth: 220, alignment: .leading) } header: {
Text("Media Format").foregroundStyle(.blue)
}
}
Section("Save Location") {
Toggle("Use one folder for all files", isOn: $overrideDestination)
if overrideDestination {
HStack(spacing: 8) {
TextField("Automatic by file type", text: $destinationPath)
.textFieldStyle(.roundedBorder)
.font(.system(.callout, design: .monospaced))
Button {
selectDestination()
} label: {
Label("Select...", systemImage: "folder.badge.plus")
}
}
}
}
Section("Queue") {
Picker("Target Queue", selection: $targetQueueID) {
ForEach(controller.queues) { queue in
Text(queue.name).tag(queue.id)
}
}
.labelsHidden()
}
Section("Transfer Settings") {
VStack(alignment: .leading, spacing: 4) {
Text("Connections per File")
HStack {
Slider(value: $connectionsPerServer, in: 1...16, step: 1)
Text("\(Int(connectionsPerServer))")
.monospacedDigit()
.frame(width: 24, alignment: .trailing)
}
}
Toggle("Limit speed per file", isOn: $speedLimitEnabled)
if speedLimitEnabled {
Stepper(
"\(speedLimitKiBPerSecond) KiB/s",
value: $speedLimitKiBPerSecond,
in: 1...10_485_760,
step: 128
)
}
}
Section("Authorization") {
Toggle("Use authorization", isOn: $useAuthorization)
if useAuthorization {
TextField("Username", text: $authUsername)
.textFieldStyle(.roundedBorder)
SecureField("Password", text: $authPassword)
.textFieldStyle(.roundedBorder)
Toggle("Save login for this website", isOn: $saveLogin)
.font(.caption)
.foregroundStyle(.secondary)
} }
} }
} }
@@ -344,61 +324,53 @@ struct AddDownloadsView: View {
} }
private var advancedTransferSection: some View { private var advancedTransferSection: some View {
DisclosureGroup(isExpanded: $showsAdvancedTransfer) { Section {
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 8) { DisclosureGroup(isExpanded: $showsAdvancedTransfer) {
GridRow(alignment: .firstTextBaseline) { VStack(alignment: .leading, spacing: 12) {
Toggle("Checksum", isOn: $checksumEnabled) Toggle("Verify Checksum", isOn: $checksumEnabled)
.font(.subheadline.weight(.semibold)) if checksumEnabled {
HStack(spacing: 8) {
Picker("Algorithm", selection: $checksumAlgorithm) { Picker("Algorithm", selection: $checksumAlgorithm) {
ForEach(ChecksumAlgorithm.allCases) { algorithm in ForEach(ChecksumAlgorithm.allCases) { algorithm in
Text(algorithm.title).tag(algorithm) Text(algorithm.title).tag(algorithm)
} }
} }
.labelsHidden()
.frame(width: 130)
TextField("Expected digest", text: $checksumValue) TextField("Expected digest", text: $checksumValue)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.font(.system(.callout, design: .monospaced)) .font(.system(.callout, design: .monospaced))
} }
.disabled(!checksumEnabled)
}
GridRow(alignment: .top) { VStack(alignment: .leading, spacing: 4) {
Label("Headers", systemImage: "text.quote") Text("Headers")
.font(.subheadline.weight(.semibold)) TextEditor(text: $headerText)
TextEditor(text: $headerText) .font(.system(.callout, design: .monospaced))
.font(.system(.callout, design: .monospaced)) .scrollContentBackground(.hidden)
.scrollContentBackground(.hidden) .background(.quaternary.opacity(0.35))
.background(.quaternary.opacity(0.35)) .clipShape(RoundedRectangle(cornerRadius: 8))
.clipShape(RoundedRectangle(cornerRadius: 8)) .frame(minHeight: 48)
.frame(minHeight: 48) }
}
GridRow(alignment: .firstTextBaseline) { VStack(alignment: .leading, spacing: 4) {
Label("Cookies", systemImage: "circle.hexagongrid.circle") Text("Cookies")
.font(.subheadline.weight(.semibold)) TextField("name=value; other=value", text: $cookieText)
TextField("name=value; other=value", text: $cookieText) .textFieldStyle(.roundedBorder)
.textFieldStyle(.roundedBorder) .font(.system(.callout, design: .monospaced))
.font(.system(.callout, design: .monospaced)) }
}
GridRow(alignment: .top) { VStack(alignment: .leading, spacing: 4) {
Label("Mirrors", systemImage: "point.3.filled.connected.trianglepath.dotted") Text("Mirrors")
.font(.subheadline.weight(.semibold)) TextEditor(text: $mirrorText)
TextEditor(text: $mirrorText) .font(.system(.callout, design: .monospaced))
.font(.system(.callout, design: .monospaced)) .scrollContentBackground(.hidden)
.scrollContentBackground(.hidden) .background(.quaternary.opacity(0.35))
.background(.quaternary.opacity(0.35)) .clipShape(RoundedRectangle(cornerRadius: 8))
.clipShape(RoundedRectangle(cornerRadius: 8)) .frame(minHeight: 48)
.frame(minHeight: 48) }
} }
.padding(.top, 8)
} label: {
Label("Advanced Transfer", systemImage: "slider.horizontal.3")
.font(.subheadline.weight(.semibold))
} }
.padding(.top, 8)
} label: {
Label("Advanced Transfer", systemImage: "slider.horizontal.3")
.font(.subheadline.weight(.semibold))
} }
} }
@@ -11,7 +11,7 @@ enum KeychainCredentialStore {
kSecAttrAccount as String: id.uuidString, kSecAttrAccount as String: id.uuidString,
kSecReturnData as String: true, kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne, kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationUI as String: kSecUseAuthenticationUISkip kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
] ]
var result: CFTypeRef? var result: CFTypeRef?
@@ -59,7 +59,7 @@ enum KeychainCredentialStore {
kSecAttrAccount as String: extensionTokenAccount, kSecAttrAccount as String: extensionTokenAccount,
kSecReturnData as String: true, kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne, kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationUI as String: kSecUseAuthenticationUISkip kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
] ]
var result: CFTypeRef? var result: CFTypeRef?