mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-31 04:58:11 +00:00
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.
This commit is contained in:
@@ -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/),
|
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).
|
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
|
## [0.5.5] - 2026-06-05
|
||||||
|
|
||||||
### New features
|
### New features
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
@@ -20,6 +20,7 @@ mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
|
|||||||
cp ".build/$CONFIGURATION/$APP_NAME" "$MACOS_DIR/$APP_NAME"
|
cp ".build/$CONFIGURATION/$APP_NAME" "$MACOS_DIR/$APP_NAME"
|
||||||
cp "$ROOT_DIR/Resources/$ICON_NAME.icns" "$RESOURCES_DIR/$ICON_NAME.icns"
|
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/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..."
|
echo "Packaging Firefox extension..."
|
||||||
mkdir -p "$RESOURCES_DIR/FirefoxExtension"
|
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
|
adjusted.credentials?.password = storedPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if adjusted.status == .completed && adjusted.progress != 1 {
|
||||||
|
adjusted.progress = 1
|
||||||
|
shouldRewriteStoredDownloads = true
|
||||||
|
}
|
||||||
|
|
||||||
if adjusted.status == .downloading {
|
if adjusted.status == .downloading {
|
||||||
adjusted.status = .queued
|
adjusted.status = .queued
|
||||||
adjusted.message = "Recovered after restart. Resuming from partial file."
|
adjusted.message = "Recovered after restart. Resuming from partial file."
|
||||||
|
|||||||
@@ -437,11 +437,11 @@ private struct DownloadSummaryHeader: View {
|
|||||||
.foregroundStyle(statusColor)
|
.foregroundStyle(statusColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressView(value: item.progress)
|
ProgressView(value: item.status == .completed ? 1.0 : item.progress)
|
||||||
|
|
||||||
Grid(alignment: .leading, horizontalSpacing: 18, verticalSpacing: 5) {
|
Grid(alignment: .leading, horizontalSpacing: 18, verticalSpacing: 5) {
|
||||||
GridRow {
|
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("Size", ByteFormatter.string(item.sizeBytes))
|
||||||
summary("Speed", item.speedText)
|
summary("Speed", item.speedText)
|
||||||
summary("ETA", item.etaText)
|
summary("ETA", item.etaText)
|
||||||
|
|||||||
@@ -21,95 +21,103 @@ struct AboutSettingsPane: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
HStack(alignment: .center, spacing: 14) {
|
HStack(alignment: .center, spacing: 16) {
|
||||||
Image(nsImage: NSApp.applicationIconImage)
|
Image(nsImage: NSApp.applicationIconImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 56, height: 56)
|
.frame(width: 64, height: 64)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Firelink")
|
Text("Firelink")
|
||||||
.font(.title2.weight(.semibold))
|
.font(.title2.weight(.bold))
|
||||||
Text("Version \(appVersion)")
|
Text("Version \(appVersion) (\(buildNumber))")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text("A native macOS download manager for fast, organized, segmented transfers.")
|
Text("A native macOS download manager for fast, organized, segmented transfers.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Updates") {
|
Section("Updates") {
|
||||||
LabeledContent("Status") {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Label(updateChecker.status.message, systemImage: updateStatusSymbol)
|
HStack {
|
||||||
.foregroundStyle(updateStatusColor)
|
Label(updateChecker.status.message, systemImage: updateStatusSymbol)
|
||||||
}
|
.foregroundStyle(updateStatusColor)
|
||||||
|
|
||||||
if let lastChecked = updateChecker.lastChecked {
|
Spacer()
|
||||||
LabeledContent("Last checked") {
|
|
||||||
Text(lastChecked, format: .dateTime.month().day().hour().minute())
|
if let lastChecked = updateChecker.lastChecked {
|
||||||
.foregroundStyle(.secondary)
|
Text("Last checked: \(lastChecked, format: .dateTime.month().day().hour().minute())")
|
||||||
}
|
.font(.caption)
|
||||||
}
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
HStack {
|
|
||||||
Button {
|
|
||||||
Task {
|
|
||||||
await updateChecker.checkForUpdates(currentVersion: appVersion)
|
|
||||||
}
|
}
|
||||||
} 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 {
|
if case .updateAvailable(_, let releaseURL) = updateChecker.status {
|
||||||
Button {
|
Button {
|
||||||
NSWorkspace.shared.open(releaseURL)
|
NSWorkspace.shared.open(releaseURL)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Download Latest Version", systemImage: "square.and.arrow.down")
|
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") {
|
Section {
|
||||||
LabeledContent("Created by") {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text("NimBold")
|
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)
|
||||||
}
|
}
|
||||||
|
.font(.caption)
|
||||||
LabeledContent("Source") {
|
.padding(.vertical, 4)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
|
|||||||
@@ -7,8 +7,17 @@ struct LocationsSettingsPane: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
ForEach(DownloadCategory.allCases, id: \.self) { category in
|
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 12) {
|
||||||
DirectoryPickerRow(category: category)
|
BulkDirectoryPickerRow()
|
||||||
|
|
||||||
|
GridRow {
|
||||||
|
Divider()
|
||||||
|
.gridCellColumns(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(DownloadCategory.allCases, id: \.self) { category in
|
||||||
|
DirectoryPickerRow(category: category)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
@@ -17,6 +26,8 @@ struct LocationsSettingsPane: View {
|
|||||||
settings.resetDirectories()
|
settings.resetDirectories()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} footer: {
|
||||||
|
Text("Folders will be created automatically when applied.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
@@ -31,10 +42,14 @@ struct DirectoryPickerRow: View {
|
|||||||
@State private var message = ""
|
@State private var message = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LabeledContent {
|
GridRow(alignment: .firstTextBaseline) {
|
||||||
|
Label(category.rawValue, systemImage: category.symbolName)
|
||||||
|
.gridColumnAlignment(.leading)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
TextField("Folder path", text: $path)
|
TextField("", text: $path, prompt: Text("Folder path"))
|
||||||
|
.labelsHidden()
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
.font(.system(.body, design: .monospaced))
|
.font(.system(.body, design: .monospaced))
|
||||||
.onSubmit {
|
.onSubmit {
|
||||||
@@ -55,12 +70,12 @@ struct DirectoryPickerRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(message.isEmpty ? statusMessage(for: path) : message)
|
if let displayMessage = message.isEmpty ? statusMessage(for: path) : message, !displayMessage.isEmpty {
|
||||||
.font(.caption)
|
Text(displayMessage)
|
||||||
.foregroundStyle(message.isEmpty ? .secondary : .primary)
|
.font(.caption)
|
||||||
|
.foregroundStyle(isErrorMessage(displayMessage) ? .red : .secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
|
||||||
Label(category.rawValue, systemImage: category.symbolName)
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
syncPathFromSettings()
|
syncPathFromSettings()
|
||||||
@@ -127,7 +142,7 @@ struct DirectoryPickerRow: View {
|
|||||||
message = "Saved."
|
message = "Saved."
|
||||||
}
|
}
|
||||||
|
|
||||||
private func statusMessage(for path: String) -> String {
|
private func statusMessage(for path: String) -> String? {
|
||||||
let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !trimmed.isEmpty else { return "Enter a folder path." }
|
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 "This path points to a file, not a folder."
|
||||||
}
|
}
|
||||||
return FileManager.default.isWritableFile(atPath: expanded)
|
return FileManager.default.isWritableFile(atPath: expanded)
|
||||||
? "Ready."
|
? nil
|
||||||
: "Firelink cannot write to this folder."
|
: "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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user