Compare commits

...

1 Commits

Author SHA1 Message Date
nimbold a2135bb539 feat: redesign About pane and resolve download properties UI bugs
- Added transparent GitHub icon to About pane.

- Compacted About pane layout and consolidated footer.

- Fixed build script to correctly copy bundled image assets.

- Resolved progress bar state issues in Download Properties.

- Fixed text overlap bugs in Settings.
2026-06-05 13:56:02 +03:30
9 changed files with 261 additions and 79 deletions
+12
View File
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.6] - 2026-06-05
### New features
- Added the official transparent GitHub icon to the Source Code link in the About page.
### Changes
- Compacted the About settings pane to reduce vertical padding, placing the app identity and updates prominently at the top.
- Consolidated developer, credits, and legal links into a single unified footer section in the About pane.
### Fixes
- Fixed a build script bug that prevented bundled images (like the GitHub icon) from being copied into the final app bundle.
## [0.5.5] - 2026-06-05
### New features
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

+1
View File
@@ -20,6 +20,7 @@ mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
cp ".build/$CONFIGURATION/$APP_NAME" "$MACOS_DIR/$APP_NAME"
cp "$ROOT_DIR/Resources/$ICON_NAME.icns" "$RESOURCES_DIR/$ICON_NAME.icns"
cp "$ROOT_DIR/Sources/Firelink/Assets.xcassets/MenuBarIcon.imageset/MenuBarIconTemplate.png" "$RESOURCES_DIR/MenuBarIconTemplate.png"
cp "$ROOT_DIR/Resources/GitHubTemplate.png" "$RESOURCES_DIR/GitHubTemplate.png"
echo "Packaging Firefox extension..."
mkdir -p "$RESOURCES_DIR/FirefoxExtension"
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "github-mark.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -818,6 +818,11 @@ final class DownloadController: ObservableObject {
adjusted.credentials?.password = storedPassword
}
if adjusted.status == .completed && adjusted.progress != 1 {
adjusted.progress = 1
shouldRewriteStoredDownloads = true
}
if adjusted.status == .downloading {
adjusted.status = .queued
adjusted.message = "Recovered after restart. Resuming from partial file."
@@ -437,11 +437,11 @@ private struct DownloadSummaryHeader: View {
.foregroundStyle(statusColor)
}
ProgressView(value: item.progress)
ProgressView(value: item.status == .completed ? 1.0 : item.progress)
Grid(alignment: .leading, horizontalSpacing: 18, verticalSpacing: 5) {
GridRow {
summary("Progress", item.progress.formatted(.percent.precision(.fractionLength(0))))
summary("Progress", (item.status == .completed ? 1.0 : item.progress).formatted(.percent.precision(.fractionLength(0))))
summary("Size", ByteFormatter.string(item.sizeBytes))
summary("Speed", item.speedText)
summary("ETA", item.etaText)
@@ -21,95 +21,103 @@ struct AboutSettingsPane: View {
var body: some View {
Form {
Section {
HStack(alignment: .center, spacing: 14) {
HStack(alignment: .center, spacing: 16) {
Image(nsImage: NSApp.applicationIconImage)
.resizable()
.frame(width: 56, height: 56)
.frame(width: 64, height: 64)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 4) {
Text("Firelink")
.font(.title2.weight(.semibold))
Text("Version \(appVersion)")
.font(.title2.weight(.bold))
Text("Version \(appVersion) (\(buildNumber))")
.foregroundStyle(.secondary)
Text("A native macOS download manager for fast, organized, segmented transfers.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 4)
.padding(.vertical, 8)
}
Section("Updates") {
LabeledContent("Status") {
Label(updateChecker.status.message, systemImage: updateStatusSymbol)
.foregroundStyle(updateStatusColor)
}
if let lastChecked = updateChecker.lastChecked {
LabeledContent("Last checked") {
Text(lastChecked, format: .dateTime.month().day().hour().minute())
.foregroundStyle(.secondary)
}
}
HStack {
Button {
Task {
await updateChecker.checkForUpdates(currentVersion: appVersion)
VStack(alignment: .leading, spacing: 12) {
HStack {
Label(updateChecker.status.message, systemImage: updateStatusSymbol)
.foregroundStyle(updateStatusColor)
Spacer()
if let lastChecked = updateChecker.lastChecked {
Text("Last checked: \(lastChecked, format: .dateTime.month().day().hour().minute())")
.font(.caption)
.foregroundStyle(.secondary)
}
} label: {
Label("Check for Updates", systemImage: "arrow.clockwise")
}
.disabled(updateChecker.status == .checking)
Button {
openReleasesPage()
} label: {
Label("Open Releases", systemImage: "arrow.up.right.square")
}
Spacer()
}
HStack(spacing: 12) {
Button {
Task {
await updateChecker.checkForUpdates(currentVersion: appVersion)
}
} label: {
Label("Check for Updates", systemImage: "arrow.clockwise")
}
.disabled(updateChecker.status == .checking)
if case .updateAvailable(_, let releaseURL) = updateChecker.status {
Button {
NSWorkspace.shared.open(releaseURL)
} label: {
Label("Download Latest Version", systemImage: "square.and.arrow.down")
if case .updateAvailable(_, let releaseURL) = updateChecker.status {
Button {
NSWorkspace.shared.open(releaseURL)
} label: {
Label("Download Latest Version", systemImage: "square.and.arrow.down")
}
.buttonStyle(.borderedProminent)
} else {
Button {
openReleasesPage()
} label: {
Label("Open Releases", systemImage: "arrow.up.right.square")
}
}
}
.buttonStyle(.borderedProminent)
}
.padding(.vertical, 4)
}
Section("Developer") {
LabeledContent("Created by") {
Text("NimBold")
Section {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Created by NimBold")
Spacer()
Link(destination: projectURL) {
HStack(spacing: 4) {
if let imgPath = Bundle.main.path(forResource: "GitHubTemplate", ofType: "png"),
let nsImage = NSImage(contentsOfFile: imgPath) {
Image(nsImage: nsImage)
.resizable()
.renderingMode(.template)
.frame(width: 14, height: 14)
.foregroundStyle(.secondary)
}
Text("Source Code")
}
}
.buttonStyle(.plain)
}
HStack {
Text("Powered by")
Link("aria2", destination: aria2URL)
Spacer()
Link("MIT License", destination: licenseURL)
}
Text("Copyright © 2026 NimBold. All rights reserved.")
.font(.caption2)
.foregroundStyle(.secondary)
}
LabeledContent("Source") {
Link("nimbold/Firelink", destination: projectURL)
}
}
Section("Credits") {
LabeledContent("Download engine") {
Link("aria2", destination: aria2URL)
}
Text("Firelink uses aria2c for segmented HTTP, HTTPS, FTP, and SFTP downloads.")
.font(.caption)
.foregroundStyle(.secondary)
}
Section("Legal") {
LabeledContent("License") {
Link("MIT License", destination: licenseURL)
}
Text("Copyright © 2026 NimBold. Firelink is released under the MIT License.")
.font(.caption)
.foregroundStyle(.secondary)
.font(.caption)
.padding(.vertical, 4)
}
}
.formStyle(.grouped)
@@ -7,8 +7,17 @@ struct LocationsSettingsPane: View {
var body: some View {
Form {
Section {
ForEach(DownloadCategory.allCases, id: \.self) { category in
DirectoryPickerRow(category: category)
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 12) {
BulkDirectoryPickerRow()
GridRow {
Divider()
.gridCellColumns(2)
}
ForEach(DownloadCategory.allCases, id: \.self) { category in
DirectoryPickerRow(category: category)
}
}
HStack {
@@ -17,6 +26,8 @@ struct LocationsSettingsPane: View {
settings.resetDirectories()
}
}
} footer: {
Text("Folders will be created automatically when applied.")
}
}
.formStyle(.grouped)
@@ -31,10 +42,14 @@ struct DirectoryPickerRow: View {
@State private var message = ""
var body: some View {
LabeledContent {
GridRow(alignment: .firstTextBaseline) {
Label(category.rawValue, systemImage: category.symbolName)
.gridColumnAlignment(.leading)
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
TextField("Folder path", text: $path)
TextField("", text: $path, prompt: Text("Folder path"))
.labelsHidden()
.textFieldStyle(.roundedBorder)
.font(.system(.body, design: .monospaced))
.onSubmit {
@@ -55,12 +70,12 @@ struct DirectoryPickerRow: View {
}
}
Text(message.isEmpty ? statusMessage(for: path) : message)
.font(.caption)
.foregroundStyle(message.isEmpty ? .secondary : .primary)
if let displayMessage = message.isEmpty ? statusMessage(for: path) : message, !displayMessage.isEmpty {
Text(displayMessage)
.font(.caption)
.foregroundStyle(isErrorMessage(displayMessage) ? .red : .secondary)
}
}
} label: {
Label(category.rawValue, systemImage: category.symbolName)
}
.onAppear {
syncPathFromSettings()
@@ -127,7 +142,7 @@ struct DirectoryPickerRow: View {
message = "Saved."
}
private func statusMessage(for path: String) -> String {
private func statusMessage(for path: String) -> String? {
let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return "Enter a folder path." }
@@ -139,10 +154,136 @@ struct DirectoryPickerRow: View {
return "This path points to a file, not a folder."
}
return FileManager.default.isWritableFile(atPath: expanded)
? "Ready."
? nil
: "Firelink cannot write to this folder."
}
return "Folder will be created when applied."
return nil
}
private func isErrorMessage(_ message: String) -> Bool {
message == "This path points to a file, not a folder." ||
message.hasPrefix("Could not create folder:") ||
message == "Firelink cannot write to this folder." ||
message == "Enter a folder path."
}
}
struct BulkDirectoryPickerRow: View {
@EnvironmentObject private var settings: AppSettings
@State private var path = ""
@State private var message = ""
var body: some View {
GridRow(alignment: .firstTextBaseline) {
Label("All Categories", systemImage: "folder.fill.badge.plus")
.gridColumnAlignment(.leading)
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
TextField("", text: $path, prompt: Text("Base folder path"))
.labelsHidden()
.textFieldStyle(.roundedBorder)
.font(.system(.body, design: .monospaced))
.onSubmit {
applyPath()
}
Button {
applyPath()
} label: {
Label("Apply", systemImage: "checkmark")
}
.disabled(path.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
Button {
selectFolder()
} label: {
Label("Select", systemImage: "folder.badge.plus")
}
}
if !message.isEmpty {
Text(message)
.font(.caption)
.foregroundStyle(isErrorMessage(message) ? .red : .secondary)
} else {
Text("Automatically creates all category folders in the selected path.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
private func selectFolder() {
let panel = NSOpenPanel()
panel.canChooseFiles = false
panel.canChooseDirectories = true
panel.allowsMultipleSelection = false
panel.canCreateDirectories = true
if panel.runModal() == .OK, let url = panel.url {
path = url.path
applyPath()
}
}
private func applyPath() {
let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
message = "Enter a base folder path."
return
}
let expanded = NSString(string: trimmed).expandingTildeInPath
var isDirectory: ObjCBool = false
if FileManager.default.fileExists(atPath: expanded, isDirectory: &isDirectory) {
guard isDirectory.boolValue else {
message = "This path points to a file, not a folder."
return
}
} else {
do {
try FileManager.default.createDirectory(
at: URL(fileURLWithPath: expanded, isDirectory: true),
withIntermediateDirectories: true
)
} catch {
message = "Could not create folder: \(error.localizedDescription)"
return
}
}
guard FileManager.default.isWritableFile(atPath: expanded) else {
message = "Firelink cannot write to this folder."
return
}
for category in DownloadCategory.allCases {
let categoryPath = (expanded as NSString).appendingPathComponent(category.rawValue)
do {
try FileManager.default.createDirectory(
at: URL(fileURLWithPath: categoryPath, isDirectory: true),
withIntermediateDirectories: true
)
settings.setDirectory(categoryPath, for: category)
} catch {
message = "Could not create category folder \(category.rawValue): \(error.localizedDescription)"
return
}
}
message = "Created all categories in base folder."
path = ""
}
private func isErrorMessage(_ message: String) -> Bool {
message == "This path points to a file, not a folder." ||
message.hasPrefix("Could not create folder:") ||
message.hasPrefix("Could not create category folder") ||
message == "Firelink cannot write to this folder." ||
message == "Enter a base folder path."
}
}