From a2135bb5396a43302d39996af5bfe6b5cdf77b8a Mon Sep 17 00:00:00 2001 From: nimbold <11913706+nimbold@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:56:02 +0330 Subject: [PATCH] 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. --- CHANGELOG.md | 12 ++ Resources/GitHubTemplate.png | Bin 0 -> 5492 bytes Scripts/create_app_bundle.sh | 1 + .../GitHub.imageset/Contents.json | 15 ++ .../GitHub.imageset/github-mark.png | Bin 0 -> 7249 bytes Sources/Firelink/DownloadController.swift | 5 + Sources/Firelink/DownloadPropertiesView.swift | 4 +- .../Firelink/Settings/AboutSettingsPane.swift | 138 ++++++++------- .../Settings/LocationsSettingsPane.swift | 165 ++++++++++++++++-- 9 files changed, 261 insertions(+), 79 deletions(-) create mode 100644 Resources/GitHubTemplate.png create mode 100644 Sources/Firelink/Assets.xcassets/GitHub.imageset/Contents.json create mode 100644 Sources/Firelink/Assets.xcassets/GitHub.imageset/github-mark.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ad7c35..5794800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Resources/GitHubTemplate.png b/Resources/GitHubTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..d045356857988d2f7810ddbdf98a206ab449a54f GIT binary patch literal 5492 zcmY*dbyU>BxBs#%9U={~bVy0pA|*&mmxLeqP|s_qC>aId?Baf3=Bn@{@%cm&6yyiqBAmGUjzdqa zC{Ym-E)$J-T1tD-)Z9#Ghou}Br(?)$=yjqGLAt!0Moq_#!Wv;x2h*I#Sfm$ylW?IO z3KmU@B{xTodYr-uih}V{-yo65jefh|&*S#`hj~kdN-Lv%gnL@Xn-R z0j#X7JV;LA_t2Lfr`slp2xg@6k!<5*vX!b1oBhXf>=T|JrU1;Jy`$V;3bwz#_>SduO+e3070(AGGtCU0v6ze7E2|IHaE$NSM6PTaLf{Q1#Pj?`KLZYU!Rf zA3X6aD)iHu@z{m2s83)2&hAzdb@{@&*r3?2xI;V+bjFH(#PwKXX6DFgS67XdZFxeA z07Fq}=}K0QqRl0k1}5F!I?-eD@nM8b zdSGNfeniLa+ljh#oRJCaTnI^Gdwn{-Nv;5BDKWgb_{m1dXX>UmoB-#V6dvfr;82%8 zO5a$hv+*R-4o!lLj<%kJ#UBI7OZf}y!GKb0)9VVZ>w$ZuT>>v8I=khdyS5od3rfK^ zp(ro^ANhOCb{*QaRHSonUOsz-6;6)IZFgL9l!b-D2j{+M`h9;&D83&8d*O4~zmk?V zJkV6VQSh* zNqOhc%0W#mxA(d;#o+NBTsqIi%Zu$H1yJR0kBA1F2UB$XRa1cQx`g=b>qqhN#%0gu zA)tsaHo6rh9|QFB6`>)frM=(gDxQ^_AQ3JPTwpG-W4xCYrUeCJihEz9oLG;{Ya-It zocndQS(w~xWa{VQR5I(>8^haWzp!YJO<>k3Bw=0{HW zzlX}i-#&`i%-i4bEq82QKVtlNT-Ut8xtbs&m{xX}j&uJg$~K?H6w~O>Nz-@0&|MTw zm@J_x-4bc@1(M@B^+G5qYwiKG^x|x)ma4T5YVC%UWWj%Auuowfqf1Qb$mW@kvs|&T zxA0m|w?oW=K;y<;QtepKthU$PZT6Assqlf-w!B5-*51UU+#L8P2J*1BF#A=6;WcUT zO?}5+*jL_=2hc7HabA7`WOP_cJU7o!CM`|p{o~j(Q&SU@G7iIU@ zX*(MT!i(2=Q@6ocQG-|hx&*s|*rz8C{TZWj%*{N%YM>w8*I6E)fJD9p_lnxTk6h9O zDM$E6sC863-kW@LrX#kq8B?Vjhw68Wa+x3>wq z0uA|vq($L_AzkCsYH<=Y5=lNYv!J;9gD48`t-)zfP>{}1RA;ICSO{M{hm$Y3T^WGy z_hyu|;oVVV|Ay}Hogkw7#sI8)Ad4y2d8qGsme{c6MB zSxmwhrL_0R6)*K1F8|gnrl!kDM_Z5{Xnh!cr?Wp#*hd+67+>nBL0jv!apL2zPG@RD z2yK-QqUX@ZVDX?!f8y%?FcMo{qwgFEdnQL;$gz2yx5HkUKGA%kC zb;)-Q@f5{Y#z_o5^U4>OdKcJc8h=a7mopbpKNVgV_Zb3gP%mi6UB?qPiX5+Lh6^gR zxTtKukkYWk3Hi(oGXO0jKCtRXeAO12esXm#{*{ZygAc*D9goV%PDO9mR}&E`D95lt z5fCUfhu~GJah1X=DG!4x7FG(IjK3mzUQOiUSx)rN=N_LkFvNR26$5xzNUes{VtHTo z!6S#84wC|8b1?Q>yX=eSPpz2Vm9d$5x zQ0Pbo7}S?JLDXV&hTAu7EJ+?xO*ud1>|Yyq!(r9xYMLp*#vGx|Ju}Jgf$x39z@Py< z@Er7I;axy`C{NKz8{OlSr&&N94iW-{ndMZxGE#$u z6iW^vyd{*f;$qL!c33tL3Y8k#NDna(f=-;SbxADuU?;y>3j6oGK^yE`(_oV^_=y1r zgWs*tXH;>@U)HD=zb+~ztx`H_WiBwdj2!@k6F#|8$Pbhb2stlWqzMGd%^n(cy<$x0 z--iVOS@jAY`hr>MCjM=*da%%0*TQCY@^|GST4DRWDU?7f3^q5LhHB0SS$|)~(+-S<gyGHaa@*yHCU|^FHPW7{4oUaF0@h|uZL#vRk2SqIo>oR{eVJK6QF>6r(;`I z%|ay-X!$N0iahKCg3j$w*&pxnZ~^o+YQC(B6OhtZ9#0NexoDs}|JjaJuYcs#boXOu zLWNg>$4`6fxFkamg4;$VS}&;HBf>)yTdJ%>_9y+WVoyse@x_1YYjgHBi%Rxou6MeQ zBB2{PD@TlZ_4&r(AX-At^pvWT-0f$KBnnC8e?5w0!6bJZ|=Vw1#Ht$@YmuYt>c> z%wvm%vB{XG*>x3n=*_FT`I+J1m;jD=0U_BJph$}E(SEpb3HKOiE;HO^GQP857vkM- zDl8PB2-%dBC3s4115pgWea@XO1&M2p1KfwTpI@@jpQ+{zI z3HeCil;~5n`UM(2?f+Re%}rITe=Xl9WOaG6v)ynGv6Rv{=8ruL>`8Vs8M|3>3-~nz zjN>&r@t2+F*n^&Bhu+&=dO4pLnC^O=@l{tqz(k+rAf$2UtxuR#w6rSf zd)XgfM-*z@fE0R<7LQyiB=p#ac?=iC4@{R%4bwe+JHVtTV;92#f&*@}fno>>(QwcH z9ne`D$G*ceFxu7DwQ`)9$<1yjkaA6dD{htl%`UT&KfW${Ru_=hQvLi9cHXzoQwk^R~@-u^Vk+W*s`^%_w`pGTs3>!ZTZL4Xj^slz0)5<{YER{mdgX5 z$vbZ}Bs`YXe0*gQu!#-}E?rN0yc;{mrNXq)Idup)-b|_8z-G5#8z)qM>0etk1KFFgL0^Lv#`lyxS|% z@R3v64?g?7Av~zicy-NUh#l4$GnD&u@H@BA>-Rca7umky8Dn~1g}ObYrBox3QJ$k^ zB2W(QlUOCu>)UMrpHPrKWziv#AqVJn7F(+FL}{U}_WTaNw>LM;xYv(xjt%2EqjbCRIfxA_sF^bTwtIVftB9r79Kz=h4AyH_x%gI|>}NjVqS7(;qym;P&FXXQ-rp@Z z)Ya6o!!alHcZPjavk0s;f}yTwaDBo!e5z=(=lwaG0ZpUBR4{3HX}Nb3$@DQZ{OF04 zttB3i0CU~t2K@ih%P7@Od0@p{T)e%#S(>fyu)}E>mNY*pWw^6uJ4(Ukw>KyK@(-(4 zh0+>BE8^Sf+LozQRdVld^OZJ{0i_DmK_IrtX5-X}Bd?E&m*vaz0V}UrNvO3pYOloL zsQ0n4C&hoo+yl8cc`rIwPf|l>j*~RG`?HHa;1(xu7x_Yf5|YGb+L1yZ)TB7gQ%rPi zSiZ5UD*jz=JspvpTq#|Md<%ZTLZRQnAJd3_FXl(kdWB)1=CV&1Sk2^UQaONW z>J;fWSDW$rvBFI_^dt-#LzF>vdU4c(LJ5i!tf%xm2(PJ&Hgvo29|=uqpW}KAH_sA- zNbqyZ|LewEVoJ`ALT|fUc+S|(ZzS$$kZj_0^HT@=w>wn6e1yxztFwT@)7cQ((KF_a z%+l4-i38Ph;?lhKn9*RFFz|V~(;wcyla4$7v)G^SW}Rk9FUJ1f=K~@chAznE0;RAm zJSCZamY1!kn=_t6Ux!^CiP|6Yc=23;1Jkt~Pe?(wM2Sx1yr;-KvD8zrP=k4YgAm#_ zpxu)HZwI4B`uiSR>uhrFaZE4SF+*Gs4CF|!@1wv68ySfDYHbb*lvG06p`ARhnFE-~ zs!lB)2Q@WG){PvnhYj;guUwLW=Qs0)Nrlo*G>=_!0_n_sx7e~?T_J59sPl1Da9r2D zRK{xFk&)!_sSLK}<%QKz1-V|>Mm$0m24|(T$m*;5z5!lJaQd+i^OE^6*xeu9N`FQJ zkxodaHSH}kLO;v^Bkr@M$p$DR?F4nwzY=QfvTzVye& z3Qm%c4+Y6xLb~r42xt))zNJcagM_e~6NTD%v1AFn{S`KuViTpJN$OcOymWtr(&UKNH+m9cA0+p18%%Tga&PMy)_?!rJnnb_S zth2{X+j)vUyAp7FFfcg{ni6qM8_Mp<%dF*ark?w?&aNm$g4p9)u>O|y?58B(@B3HgPp{ZB<4T|90m^#5;%U{wZ#d!JlFqk>hS+@ diJ-fuVe;Rkt8vvo2mfO{H6H7zR6nwb_%FX{YIgtt literal 0 HcmV?d00001 diff --git a/Scripts/create_app_bundle.sh b/Scripts/create_app_bundle.sh index 2ae4bfb..4744fb0 100755 --- a/Scripts/create_app_bundle.sh +++ b/Scripts/create_app_bundle.sh @@ -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" diff --git a/Sources/Firelink/Assets.xcassets/GitHub.imageset/Contents.json b/Sources/Firelink/Assets.xcassets/GitHub.imageset/Contents.json new file mode 100644 index 0000000..5f84abb --- /dev/null +++ b/Sources/Firelink/Assets.xcassets/GitHub.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "github-mark.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Sources/Firelink/Assets.xcassets/GitHub.imageset/github-mark.png b/Sources/Firelink/Assets.xcassets/GitHub.imageset/github-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..e28a8373067419c7fec89fc78ca44c0197e77ace GIT binary patch literal 7249 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 literal 0 HcmV?d00001 diff --git a/Sources/Firelink/DownloadController.swift b/Sources/Firelink/DownloadController.swift index d340b59..dd30ac6 100644 --- a/Sources/Firelink/DownloadController.swift +++ b/Sources/Firelink/DownloadController.swift @@ -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." diff --git a/Sources/Firelink/DownloadPropertiesView.swift b/Sources/Firelink/DownloadPropertiesView.swift index 1fc9282..71d14f6 100644 --- a/Sources/Firelink/DownloadPropertiesView.swift +++ b/Sources/Firelink/DownloadPropertiesView.swift @@ -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) diff --git a/Sources/Firelink/Settings/AboutSettingsPane.swift b/Sources/Firelink/Settings/AboutSettingsPane.swift index afae3fe..279e93c 100644 --- a/Sources/Firelink/Settings/AboutSettingsPane.swift +++ b/Sources/Firelink/Settings/AboutSettingsPane.swift @@ -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) diff --git a/Sources/Firelink/Settings/LocationsSettingsPane.swift b/Sources/Firelink/Settings/LocationsSettingsPane.swift index 8721558..1b0a371 100644 --- a/Sources/Firelink/Settings/LocationsSettingsPane.swift +++ b/Sources/Firelink/Settings/LocationsSettingsPane.swift @@ -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." } }