mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-04 06:55:23 +00:00
fix: keychain prompts, AddDownloadsView dynamic sizing, and options format
This commit is contained in:
@@ -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?
|
||||||
|
|||||||
Reference in New Issue
Block a user