diff --git a/Applite.xcodeproj/project.pbxproj b/Applite.xcodeproj/project.pbxproj index 7139389..1f5e349 100644 --- a/Applite.xcodeproj/project.pbxproj +++ b/Applite.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 411EDDD72A9F58180051E07B /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411EDDD62A9F58180051E07B /* URLExtension.swift */; }; 4120AB652A754B1700F68EFE /* AppliteAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4120AB642A754B1700F68EFE /* AppliteAppView.swift */; }; 4120AB682A755B5A00F68EFE /* CheckForUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4120AB672A755B5A00F68EFE /* CheckForUpdatesView.swift */; }; + 4122B4792D3574A6002E9D6E /* CaskWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4122B4782D3574A6002E9D6E /* CaskWarning.swift */; }; + 4122B47B2D3576FB002E9D6E /* AppView+IconsAndWarnings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4122B47A2D3576FB002E9D6E /* AppView+IconsAndWarnings.swift */; }; 4125BB8A29539907000FBD25 /* PlaceholderAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4125BB8929539907000FBD25 /* PlaceholderAppView.swift */; }; 4126353E2A77C6EF00155034 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4126353D2A77C6EF00155034 /* ArrayExtension.swift */; }; 412635442A77FB1600155034 /* BrewInstallationProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412635432A77FB1600155034 /* BrewInstallationProgress.swift */; }; @@ -156,6 +158,8 @@ 411EDDD62A9F58180051E07B /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = ""; }; 4120AB642A754B1700F68EFE /* AppliteAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppliteAppView.swift; sourceTree = ""; }; 4120AB672A755B5A00F68EFE /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = ""; }; + 4122B4782D3574A6002E9D6E /* CaskWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaskWarning.swift; sourceTree = ""; }; + 4122B47A2D3576FB002E9D6E /* AppView+IconsAndWarnings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppView+IconsAndWarnings.swift"; sourceTree = ""; }; 4125BB8929539907000FBD25 /* PlaceholderAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAppView.swift; sourceTree = ""; }; 4126353D2A77C6EF00155034 /* ArrayExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtension.swift; sourceTree = ""; }; 412635432A77FB1600155034 /* BrewInstallationProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewInstallationProgress.swift; sourceTree = ""; }; @@ -516,6 +520,7 @@ 4192561D2D1DEBE700D9EF10 /* AppView+UninstallButton.swift */, 4192561F2D1DEC0D00D9EF10 /* AppView+UpdateButton.swift */, 419256B52D26BBBD00D9EF10 /* AppView+GetInfoButton.swift */, + 4122B47A2D3576FB002E9D6E /* AppView+IconsAndWarnings.swift */, ); path = "App View"; sourceTree = ""; @@ -622,6 +627,7 @@ 415135652D32C4570025DB70 /* Cask+Equtable.swift */, 415135672D32C4710025DB70 /* Cask+Hashable.swift */, 415135692D32C48A0025DB70 /* Cask+Comparable.swift */, + 4122B4782D3574A6002E9D6E /* CaskWarning.swift */, ); path = Cask; sourceTree = ""; @@ -849,6 +855,7 @@ 419256572D1E0E5F00D9EF10 /* DiscoverSectionView+AppRow.swift in Sources */, 4192568F2D22DC9E00D9EF10 /* FontExtension.swift in Sources */, 419506A42964A27F00FE5802 /* SetupView.swift in Sources */, + 4122B47B2D3576FB002E9D6E /* AppView+IconsAndWarnings.swift in Sources */, 4192561E2D1DEBE700D9EF10 /* AppView+UninstallButton.swift in Sources */, 413F872E2D33E1CA00D4BE10 /* HomeView+NoSearchResults.swift in Sources */, 4192569B2D24335900D9EF10 /* CaskTaskError.swift in Sources */, @@ -892,6 +899,7 @@ 4192569D2D2433E200D9EF10 /* CaskProgressState.swift in Sources */, 418F331C28EB3D540023D76F /* AppGridView.swift in Sources */, 419256332D1DF2E000D9EF10 /* SettingsView+BrewSettingsView.swift in Sources */, + 4122B4792D3574A6002E9D6E /* CaskWarning.swift in Sources */, 419256962D2430C500D9EF10 /* CaskManager+LoadData.swift in Sources */, 419256552D1E0E2500D9EF10 /* DiscoverSectionView+CategoryHeader.swift in Sources */, 41062C972A3A20F900FD48EA /* UninstallSelf.swift in Sources */, diff --git a/Applite/Model/Cask Models/Cask/Cask.swift b/Applite/Model/Cask Models/Cask/Cask.swift index 97e4eb8..ab664a0 100755 --- a/Applite/Model/Cask Models/Cask/Cask.swift +++ b/Applite/Model/Cask Models/Cask/Cask.swift @@ -41,7 +41,7 @@ final class Cask: ObservableObject { name: "Test", description: "Test application", homepageURL: URL(string: "https://aerolite.dev/"), - caveats: nil, - pkgInstaller: false + pkgInstaller: false, + warning: nil ), downloadsIn365days: 100) } diff --git a/Applite/Model/Cask Models/Cask/CaskWarning.swift b/Applite/Model/Cask Models/Cask/CaskWarning.swift new file mode 100644 index 0000000..91bcb9e --- /dev/null +++ b/Applite/Model/Cask Models/Cask/CaskWarning.swift @@ -0,0 +1,29 @@ +// +// CaskWarning.swift +// Applite +// +// Created by Milán Várady on 2025.01.13. +// + +import SwiftUI + +enum CaskWarning: Codable { + case hasCaveat(caveat: String) + case deprecated(date: String, reason: String) + case disabled(date: String, reason: String) + + var title: LocalizedStringKey { + switch self { + case .hasCaveat: return "App has Caveats" + case .deprecated: return "App is Deprecated" + case .disabled: return "App is Disabled" + } + } + + var isDisabled: Bool { + switch self { + case .hasCaveat, .deprecated: return false + case .disabled: return true + } + } +} diff --git a/Applite/Model/Cask Models/CaskDTO.swift b/Applite/Model/Cask Models/CaskDTO.swift index fc8b7c7..df20416 100755 --- a/Applite/Model/Cask Models/CaskDTO.swift +++ b/Applite/Model/Cask Models/CaskDTO.swift @@ -13,20 +13,32 @@ struct CaskDTO: Decodable { let token: String let fullToken: String let tap: String - let desc: String? let nameArray: Array + let desc: String? let homepage: String let caveats: String? let url: String - + let deprecated: Bool + let deprecationDate: String? + let deprecationReason: String? + let disabled: Bool + let disableDate: String? + let disableReason: String? + enum CodingKeys: String, CodingKey { case token case fullToken = "full_token" case tap - case desc case nameArray = "name" + case desc case homepage case caveats case url + case deprecated + case deprecationDate = "deprecation_date" + case deprecationReason = "deprecation_reason" + case disabled + case disableDate = "disable_date" + case disableReason = "disable_reason" } } diff --git a/Applite/Model/Cask Models/CaskInfo.swift b/Applite/Model/Cask Models/CaskInfo.swift index c22ab5b..719d051 100644 --- a/Applite/Model/Cask Models/CaskInfo.swift +++ b/Applite/Model/Cask Models/CaskInfo.swift @@ -18,10 +18,9 @@ struct CaskInfo: Codable { /// Short description let description: String let homepageURL: URL? - /// Description of any caveats with the app - let caveats: String? /// If true app has a .pkg installer let pkgInstaller: Bool + let warning: CaskWarning? /// Initialize from a ``CaskDTO`` data transfer object init(from decoder: Decoder) throws { @@ -33,18 +32,27 @@ struct CaskInfo: Codable { self.name = rawData.nameArray[safeIndex: 0] ?? "N/A" self.description = rawData.desc ?? "N/A" self.homepageURL = URL(string: rawData.homepage) - self.caveats = rawData.caveats self.pkgInstaller = rawData.url.hasSuffix("pkg") + + if rawData.disabled { + self.warning = .disabled(date: rawData.disableDate ?? "N/A", reason: rawData.disableReason ?? "N/A") + } else if rawData.deprecated { + self.warning = .deprecated(date: rawData.deprecationDate ?? "N/A", reason: rawData.deprecationReason ?? "N/A") + } else if let caveat = rawData.caveats { + self.warning = .hasCaveat(caveat: caveat) + } else { + self.warning = nil + } } - init(token: String, fullToken: String, tap: String, name: String, description: String, homepageURL: URL?, caveats: String?, pkgInstaller: Bool) { + init(token: String, fullToken: String, tap: String, name: String, description: String, homepageURL: URL?, pkgInstaller: Bool, warning: CaskWarning?) { self.token = token self.fullToken = fullToken self.tap = tap self.name = name self.description = description self.homepageURL = homepageURL - self.caveats = caveats self.pkgInstaller = pkgInstaller + self.warning = warning } } diff --git a/Applite/Views/App Views/App View/AppView+DownloadButton.swift b/Applite/Views/App Views/App View/AppView+DownloadButton.swift index 40e1490..ae35cdf 100644 --- a/Applite/Views/App Views/App View/AppView+DownloadButton.swift +++ b/Applite/Views/App Views/App View/AppView+DownloadButton.swift @@ -16,27 +16,34 @@ extension AppView { // Alerts @State var showingPopover = false - @State var showingCaveats = false @State var showingBrewError = false @State var showingForceInstallConfirmation = false + @State var showCaveatsAndWarnings = false @State var buttonFill = false var body: some View { /// Download button Button { - if cask.info.caveats != nil { - // Show caveats dialog - showingCaveats = true + if cask.info.warning != nil { + // Show download confirmation + showCaveatsAndWarnings = true return } caskManager.install(cask) } label: { - Image(systemName: "arrow.down.to.line.circle\(buttonFill ? ".fill" : "")") - .font(.system(size: 22)) - .foregroundColor(.accentColor) + if case .disabled(_, _) = cask.info.warning { + Image(systemName: "xmark.circle") + .foregroundStyle(.red) + .font(.system(size: 22)) + } else { + Image(systemName: "arrow.down.to.line.circle\(buttonFill ? ".fill" : "")") + .foregroundStyle(Color.accentColor) + .font(.system(size: 22)) + } } + .disabled(cask.info.warning?.isDisabled ?? false) .padding(.trailing, -8) .onHover { isHovering in // Hover effect @@ -44,14 +51,23 @@ extension AppView { buttonFill = isHovering } } - .alert("App caveats", isPresented: $showingCaveats) { + .alert(cask.info.warning?.title ?? "", isPresented: $showCaveatsAndWarnings) { Button("Download Anyway") { caskManager.install(cask) } Button("Cancel", role: .cancel) { } } message: { - Text(cask.info.caveats ?? "") + if let warning = cask.info.warning { + switch warning { + case .hasCaveat(let caveat): + Text(caveat) + case .deprecated(let date, let reason): + Text("**This app is deprecated**\n**Reason:** \(reason)\n**Date:** \(date)") + case .disabled(let date, let reason): + Text("**This app is disabled**\n**Reason:** \(reason)\n**Date:** \(date)") + } + } } .alert("Broken Brew Path", isPresented: $showingBrewError) {} message: { Text(DependencyManager.brokenPathOrIstallMessage) diff --git a/Applite/Views/App Views/App View/AppView+IconAndDescriptionView.swift b/Applite/Views/App Views/App View/AppView+IconAndDescriptionView.swift index a89d3d8..bc6b34f 100644 --- a/Applite/Views/App Views/App View/AppView+IconAndDescriptionView.swift +++ b/Applite/Views/App Views/App View/AppView+IconAndDescriptionView.swift @@ -8,29 +8,33 @@ import SwiftUI extension AppView { - var iconAndDescriptionView: some View { - HStack { - if let iconURL = URL(string: "https://github.com/App-Fair/appcasks/releases/download/cask-\(cask.info.token)/AppIcon.png"), - let faviconURL = URL(string: "https://icon.horse/icon/\(cask.info.homepageURL?.host ?? "")") { - AppIconView( - iconURL: iconURL, - faviconURL: faviconURL, - cacheKey: cask.info.token - ) - .padding(.leading, 5) - } - - // Name and description - VStack(alignment: .leading) { - Text(cask.info.name) - .font(.system(size: 16, weight: .bold)) + struct IconAndDescriptionView: View { + @ObservedObject var cask: Cask - Text(cask.info.description) - .foregroundColor(.secondary) + var body: some View { + HStack { + if let iconURL = URL(string: "https://github.com/App-Fair/appcasks/releases/download/cask-\(cask.info.token)/AppIcon.png"), + let faviconURL = URL(string: "https://icon.horse/icon/\(cask.info.homepageURL?.host ?? "")") { + AppIconView( + iconURL: iconURL, + faviconURL: faviconURL, + cacheKey: cask.info.token + ) + .padding(.leading, 5) + } + + // Name and description + VStack(alignment: .leading) { + Text(cask.info.name) + .font(.system(size: 16, weight: .bold)) + + Text(cask.info.description) + .foregroundColor(.secondary) + } + + Spacer() } - - Spacer() + .contentShape(Rectangle()) } - .contentShape(Rectangle()) } } diff --git a/Applite/Views/App Views/App View/AppView+IconsAndWarnings.swift b/Applite/Views/App Views/App View/AppView+IconsAndWarnings.swift new file mode 100644 index 0000000..a81d21e --- /dev/null +++ b/Applite/Views/App Views/App View/AppView+IconsAndWarnings.swift @@ -0,0 +1,52 @@ +// +// AppView+IconsAndWarnings.swift +// Applite +// +// Created by Milán Várady on 2025.01.13. +// + +import SwiftUI + +extension AppView { + struct IconsAndWarnings: View { + @ObservedObject var cask: Cask + + var body: some View { + // Show tap icon if from a third-party tap + if cask.info.tap != "homebrew/cask" { + InfoPopup( + text: "This app is from a third-party tap:\n`\(cask.info.tap)`", + sfSymbol: "spigot.fill" + ) + .controlSize(.large) + } + + if let warning = cask.info.warning { + Group { + switch warning { + case .hasCaveat(let caveat): + InfoPopup( + text: LocalizedStringKey(caveat), + sfSymbol: "exclamationmark.circle" + ) + + case .deprecated(let date, let reason): + InfoPopup( + text: "**This app is deprecated**\n**Reason:** \(reason)\n**Date:** \(date)", + sfSymbol: "exclamationmark.triangle.fill", + color: .orange + ) + + case .disabled(let date, let reason): + InfoPopup( + text: "**This app is disabled**\n**Reason:** \(reason)\n**Date:** \(date)", + sfSymbol: "exclamationmark.triangle.fill", + color: .red + ) + } + } + .imageScale(.large) + } + } + } +} diff --git a/Applite/Views/App Views/App View/AppView.swift b/Applite/Views/App Views/App View/AppView.swift index 43f6472..30bf32f 100755 --- a/Applite/Views/App Views/App View/AppView.swift +++ b/Applite/Views/App Views/App View/AppView.swift @@ -38,17 +38,8 @@ struct AppView: View { var body: some View { HStack { - // Icon name and description - iconAndDescriptionView - - // Show tap icon if from a third-party tap - if cask.info.tap != "homebrew/cask" { - Image(systemName: "spigot.fill") - .controlSize(.large) - .help("This app is from a third-party tap") - } - - // Buttons + IconAndDescriptionView(cask: cask) + IconsAndWarnings(cask: cask) actionsView } .buttonStyle(.plain) diff --git a/Applite/Views/Components/InfoPopup.swift b/Applite/Views/Components/InfoPopup.swift index 63eed54..a78759c 100644 --- a/Applite/Views/Components/InfoPopup.swift +++ b/Applite/Views/Components/InfoPopup.swift @@ -9,23 +9,31 @@ import SwiftUI struct InfoPopup: View { let text: LocalizedStringKey + let sfSymbol: String + let color: Color @State var showPopover: Bool = false + init(text: LocalizedStringKey, sfSymbol: String = "info.circle", color: Color = .primary) { + self.text = text + self.sfSymbol = sfSymbol + self.color = color + } + var body: some View { - Button { - showPopover = true - } label: { - Image(systemName: "info.circle") - } - .buttonStyle(.plain) - .popover(isPresented: $showPopover) { - Text(text) - .textSelection(.enabled) - .frame(maxWidth: 400) - .fixedSize(horizontal: true, vertical: true) - .padding(16) - } + Image(systemName: sfSymbol) + .foregroundStyle(color) + .onHover { hover in + showPopover = hover + } + .buttonStyle(.plain) + .popover(isPresented: $showPopover) { + Text(text) + .textSelection(.enabled) + .frame(maxWidth: 400) + .fixedSize(horizontal: true, vertical: true) + .padding(16) + } } } diff --git a/Localizable.xcstrings b/Localizable.xcstrings index e03d510..2114cd5 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -170,6 +170,26 @@ } } }, + "**This app is deprecated**\n**Reason:** %@\n**Date:** %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "**This app is deprecated**\n**Reason:** %1$@\n**Date:** %2$@" + } + } + } + }, + "**This app is disabled**\n**Reason:** %@\n**Date:** %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "**This app is disabled**\n**Reason:** %1$@\n**Date:** %2$@" + } + } + } + }, "**Tip:** You can also import apps from a Brewfile. However, only casks will be installed, other items like formulae and taps will be skipped." : { "comment" : "App Migration import card tip", "localizations" : { @@ -730,6 +750,7 @@ }, "App caveats" : { "comment" : "App caveats alert title", + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -762,6 +783,15 @@ } } } + }, + "App has Caveats" : { + + }, + "App is Deprecated" : { + + }, + "App is Disabled" : { + }, "App Migration" : { "comment" : "App Migration view title", @@ -4917,6 +4947,7 @@ } }, "This app is from a third-party tap" : { + "extractionState" : "stale", "localizations" : { "hu" : { "stringUnit" : { @@ -4925,6 +4956,9 @@ } } } + }, + "This app is from a third-party tap:\n`%@`" : { + }, "This application uses the [Homebrew](https://brew.sh/) (brew for short) package manager to download apps. Homebrew is a free and open source command line utility that can download useful developer tools as well as desktop applications." : { "comment" : "Manage Homebrew view description",