1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-06 19:40:08 +02:00

⬆️ Adopt #Preview, cleanup PHP Version Manager

This commit is contained in:
2023-11-07 17:56:38 +01:00
parent 9a3dd2fa22
commit a634d083a6
18 changed files with 465 additions and 411 deletions

View File

@ -122,6 +122,10 @@
C41F3D08298AED0D0042ACBF /* System.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D3660A29113F20006BD146 /* System.swift */; };
C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4205A7D27F4D21800191A39 /* ValetProxy.swift */; };
C4205A7F27F4D21800191A39 /* ValetProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4205A7D27F4D21800191A39 /* ValetProxy.swift */; };
C42106662AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42106652AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift */; };
C42106672AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42106652AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift */; };
C42106682AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42106652AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift */; };
C42106692AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42106652AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift */; };
C422DDAA28A2C49900CEAC97 /* PhpDoctorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C422DDA928A2C49900CEAC97 /* PhpDoctorView.swift */; };
C4232EE52612526500158FC6 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = C4232EE42612526500158FC6 /* Credits.html */; };
C42337A3281F19F000459A48 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; };
@ -938,6 +942,7 @@
C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = "<group>"; };
C41E87192763D42300161EE0 /* DomainListVC+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DomainListVC+ContextMenu.swift"; sourceTree = "<group>"; };
C4205A7D27F4D21800191A39 /* ValetProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetProxy.swift; sourceTree = "<group>"; };
C42106652AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PhpVersionManagerView+Actions.swift"; sourceTree = "<group>"; };
C422DDA928A2C49900CEAC97 /* PhpDoctorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpDoctorView.swift; sourceTree = "<group>"; };
C422DDAC28A2DAC600CEAC97 /* PhpDoctorWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpDoctorWindowController.swift; sourceTree = "<group>"; };
C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = "<group>"; };
@ -1494,6 +1499,7 @@
children = (
C4D5576329C77CC5001A44CD /* PhpVersionManagerWindowController.swift */,
C43931C429C4BD610069165B /* PhpVersionManagerView.swift */,
C42106652AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift */,
C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */,
);
path = UI;
@ -2586,6 +2592,7 @@
C44067F927E2585E0045BD4E /* DomainListTypeCell.swift in Sources */,
54D9E0BA27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */,
C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */,
C42106662AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */,
C40508AF28ADA23D008FAC1F /* NoDomainResultsView.swift in Sources */,
C4B79ECB29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */,
C40D725F2A018AE30054A067 /* BrewFormula+UI.swift in Sources */,
@ -2775,6 +2782,7 @@
C471E81428F9BAE80021E251 /* NSWindowExtension.swift in Sources */,
C43BCD4629FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */,
C471E7D328F9BA8F0021E251 /* ActiveShell.swift in Sources */,
C42106682AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */,
C4B79EC829CA474200A483EE /* FakeCommand.swift in Sources */,
C471E7DE28F9BAA30021E251 /* CommandProtocol.swift in Sources */,
C471E81B28F9BB250021E251 /* BetterAlertVC.swift in Sources */,
@ -2831,6 +2839,7 @@
C471E89F28F9BB8F0021E251 /* ValetDomainScanner.swift in Sources */,
C471E8A028F9BB8F0021E251 /* FakeDomainScanner.swift in Sources */,
C471E8A228F9BB8F0021E251 /* AppDelegate.swift in Sources */,
C42106692AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */,
C43931CD29C4C03F0069165B /* Brew.swift in Sources */,
C451AFF92969E40F0078E617 /* HelpButton.swift in Sources */,
C4ACE9E429F84EDD00110766 /* PhpGuard.swift in Sources */,
@ -3083,6 +3092,7 @@
C4611E5F2AEAD2FB0010BE24 /* ConfigManagerView.swift in Sources */,
C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */,
C456A0C72AA614BD0080144F /* PhpPreference.swift in Sources */,
C42106672AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */,
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
C4FC21B128391F8E00D368BB /* MainMenu+Actions.swift in Sources */,
54D9E0B927E4F51E003B9AD9 /* KeyCombo.swift in Sources */,

View File

@ -27,15 +27,15 @@ struct HelpButton: View {
.buttonStyle(BorderlessButtonStyle())
.focusable(false)
}
struct HelpButton_Previews: PreviewProvider {
static var previews: some View {
Group {
HelpButton(action: {}).padding()
.previewDisplayName("Light Mode")
HelpButton(action: {}).padding().preferredColorScheme(.dark)
.previewDisplayName("Dark Mode")
}
}
}
}
#Preview("Light Mode") {
HelpButton(action: {})
.padding(100)
}
#Preview("Dark Mode") {
HelpButton(action: {})
.padding(100)
.preferredColorScheme(.dark)
}

View File

@ -30,8 +30,6 @@ struct NoDomainResults: View {
}
}
struct NoDomainResults_Previews: PreviewProvider {
static var previews: some View {
NoDomainResults()
}
#Preview {
NoDomainResults()
}

View File

@ -126,78 +126,82 @@ struct DisclaimerView: View {
}
}
struct VersionPopoverView_Previews: PreviewProvider {
static var previews: some View {
VersionPopoverView(
site: FakeValetSite(
fakeWithName: "amazingwebsite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: ""
),
validPhpVersions: [],
parent: nil
)
.previewDisplayName("Unknown Requirement")
VersionPopoverView(
site: FakeValetSite(
fakeWithName: "amazingwebsite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.1"
),
validPhpVersions: [],
parent: nil
)
.previewDisplayName("Requirement Matches")
VersionPopoverView(
site: FakeValetSite(
fakeWithName: "anothersite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.0",
isolated: "8.0"
),
validPhpVersions: [],
parent: nil
)
.previewDisplayName("Isolated")
VersionPopoverView(
site: FakeValetSite(
fakeWithName: "anothersite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.0",
isolated: "7.4"
),
validPhpVersions: [],
parent: nil
)
.previewDisplayName("Isolated Mismatch")
VersionPopoverView(
site: FakeValetSite(
fakeWithName: "anothersite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.0"
),
validPhpVersions: [
VersionNumber(major: 8, minor: 0, patch: 0),
VersionNumber(major: 8, minor: 1, patch: 0)
],
parent: nil
)
.previewDisplayName("Recommend Alternatives")
}
#Preview("Unknown Requirement") {
VersionPopoverView(
site: FakeValetSite(
fakeWithName: "amazingwebsite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: ""
),
validPhpVersions: [],
parent: nil
)
}
#Preview("Requirement Matches") {
VersionPopoverView(
site: FakeValetSite(
fakeWithName: "amazingwebsite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.1"
),
validPhpVersions: [],
parent: nil
)
}
#Preview("Isolated") {
VersionPopoverView(
site: FakeValetSite(
fakeWithName: "anothersite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.0",
isolated: "8.0"
),
validPhpVersions: [],
parent: nil
)
}
#Preview("Isolated Mismatch") {
VersionPopoverView(
site: FakeValetSite(
fakeWithName: "anothersite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.0",
isolated: "7.4"
),
validPhpVersions: [],
parent: nil
)
}
#Preview("Recommend Alternatives") {
VersionPopoverView(
site: FakeValetSite(
fakeWithName: "anothersite",
tld: "test",
secure: true,
path: "/path/to/site",
linked: true,
constraint: "^8.0"
),
validPhpVersions: [
VersionNumber(major: 8, minor: 0, patch: 0),
VersionNumber(major: 8, minor: 1, patch: 0)
],
parent: nil
)
}

View File

@ -45,9 +45,7 @@ struct HeaderView: View {
}
}
struct HeaderView_Previews: PreviewProvider {
static var previews: some View {
HeaderView(text: "Hello world")
.frame(width: 330.0)
}
#Preview {
HeaderView(text: "Hello world")
.frame(width: 330.0)
}

View File

@ -172,23 +172,21 @@ struct ServiceView: View {
}
}
struct ServicesView_Previews: PreviewProvider {
static var previews: some View {
ServicesView(manager: FakeServicesManager(
formulae: ["php", "nginx", "dnsmasq"],
status: .active
), perRow: 4)
.frame(width: 330.0)
.previewDisplayName("Active 1")
ServicesView(manager: FakeServicesManager(
formulae: [
"php", "nginx", "dnsmasq", "thing1",
"thing2", "thing3", "thing4", "thing5"
],
status: .inactive
), perRow: 4)
.frame(width: 330.0)
.previewDisplayName("Active 2")
}
#Preview("Active 1") {
ServicesView(manager: FakeServicesManager(
formulae: ["php", "nginx", "dnsmasq"],
status: .active
), perRow: 4)
.frame(width: 330.0)
}
#Preview("Active 2") {
ServicesView(manager: FakeServicesManager(
formulae: [
"php", "nginx", "dnsmasq", "thing1",
"thing2", "thing3", "thing4", "thing5"
],
status: .inactive
), perRow: 4)
.frame(width: 330.0)
}

View File

@ -98,12 +98,10 @@ struct StatsView: View {
}
}
struct StatsView_Previews: PreviewProvider {
static var previews: some View {
StatsView(
memoryLimit: "1024 MB",
maxPostSize: "1024 MB",
maxUploadSize: "1024 MB"
).frame(height: 100)
}
#Preview {
StatsView(
memoryLimit: "1024 MB",
maxPostSize: "1024 MB",
maxUploadSize: "1024 MB"
).frame(height: 100)
}

View File

@ -53,13 +53,11 @@ struct ProgressWindowView: View {
}
}
struct ProgressWindowView_Previews: PreviewProvider {
static var previews: some View {
ProgressWindowView(
subject: ProgressViewSubject(
title: "Long running task",
description: "Please be patient"
)
#Preview {
ProgressWindowView(
subject: ProgressViewSubject(
title: "Long running task",
description: "Please be patient"
)
}
)
}

View File

@ -136,10 +136,6 @@ struct OnboardingView: View {
}
}
struct OnboardingView_Previews: PreviewProvider {
static var previews: some View {
Group {
OnboardingView()
}
}
#Preview {
OnboardingView()
}

View File

@ -98,19 +98,19 @@ struct ByteLimitView: View {
}
}
struct ByteLimitView_Previews: PreviewProvider {
static var previews: some View {
PreferenceContainer(
name: "Max Size",
description:
"Here's an extensive description that is obviously way too long but it should wrap." +
"The point of the wrapping text is that is allows us to see what's going on with the layout here."
) {
ByteLimitView(preference: BytePhpPreference(key: "max_memory"))
}.frame(width: 600, height: 200)
ConfigManagerView()
.frame(width: 600, height: .infinity)
.previewDisplayName("Config Manager")
}
#Preview("Byte Limit View") {
PreferenceContainer(
name: "Max Size",
description:
"Here's an extensive description that is obviously way too long but it should wrap." +
"The point of the wrapping text is that is allows us to see what's going on with the layout here."
) {
ByteLimitView(preference: BytePhpPreference(key: "max_memory"))
}.frame(width: 600, height: 200)
}
#Preview("Config Manager") {
ConfigManagerView()
.frame(width: 600, height: .infinity)
.previewDisplayName("Config Manager")
}

View File

@ -84,10 +84,6 @@ struct ConfigManagerView: View {
}
}
struct ConfigManagerView_Previews: PreviewProvider {
static var previews: some View {
ConfigManagerView()
.frame(width: 600)
.previewDisplayName("Live Preview")
}
#Preview {
ConfigManagerView().frame(width: 600)
}

View File

@ -34,8 +34,8 @@ class WarningManager: ObservableObject {
.trimmingCharacters(in: .whitespacesAndNewlines) == "1"
},
name: "Running PHP Monitor with Rosetta on M1",
title: "warnings.arm_compatibility.title".localized,
paragraphs: { return ["warnings.arm_compatibility.description".localized] },
title: "warnings.arm_compatibility.title",
paragraphs: { return ["warnings.arm_compatibility.description"] },
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-and-Apple-Silicon"
),
Warning(
@ -44,11 +44,11 @@ class WarningManager: ObservableObject {
!FileSystem.isWriteableFile("/usr/local/bin/")
},
name: "Helpers cannot be symlinked and not in PATH",
title: "warnings.helper_permissions.title".localized,
title: "warnings.helper_permissions.title",
paragraphs: { return [
"warnings.helper_permissions.description".localized,
"warnings.helper_permissions.unavailable".localized,
"warnings.helper_permissions.symlink".localized
"warnings.helper_permissions.description",
"warnings.helper_permissions.unavailable",
"warnings.helper_permissions.symlink"
] },
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries"
),
@ -58,7 +58,7 @@ class WarningManager: ObservableObject {
return !PhpConfigChecker.shared.missing.isEmpty
},
name: "Your PHP installation is missing configuration files",
title: "warnings.files_missing.title".localized,
title: "warnings.files_missing.title",
paragraphs: { return [
"warnings.files_missing.description".localized(
PhpConfigChecker.shared.missing.joined(separator: "\n")

View File

@ -25,8 +25,6 @@ struct NoWarningsView: View {
}
}
struct NoWarningsView_Previews: PreviewProvider {
static var previews: some View {
NoWarningsView()
}
#Preview {
NoWarningsView().padding()
}

View File

@ -94,14 +94,12 @@ struct PhpDoctorView: View {
}
}
struct WarningListView_Previews: PreviewProvider {
static var previews: some View {
PhpDoctorView(empty: true, fake: true, manager: WarningManager())
.frame(width: 600, height: 480)
.previewDisplayName("Empty List")
PhpDoctorView(empty: false, fake: true, manager: WarningManager())
.frame(width: 600, height: 480)
.previewDisplayName("List With All Warnings")
}
#Preview("Empty List") {
PhpDoctorView(empty: true, fake: true, manager: WarningManager())
.frame(width: 600, height: 480)
}
#Preview("List With All Warnings") {
PhpDoctorView(empty: false, fake: true, manager: WarningManager())
.frame(width: 600, height: 480)
}

View File

@ -26,7 +26,7 @@ struct WarningView: View {
Text(title.localizedForSwiftUI)
.fontWeight(.bold)
ForEach(paragraphs, id: \.self) { paragraph in
Text(paragraph)
Text(paragraph.localizedForSwiftUI)
.font(.system(size: 13))
}
}
@ -47,23 +47,23 @@ struct WarningView: View {
}
}
struct WarningView_Previews: PreviewProvider {
static var previews: some View {
WarningView(
title: "warnings.helper_permissions.title",
paragraphs: ["warnings.helper_permissions.description"],
documentationUrl: "https://nicoverbruggen.be"
)
.frame(width: 600, height: 105)
WarningView(
title: "warnings.helper_permissions.title",
paragraphs: ["warnings.helper_permissions.description"],
documentationUrl: "https://nicoverbruggen.be"
)
.preferredColorScheme(.dark)
.frame(width: 600, height: 105)
// WarningListView().frame(width: 600, height: 580)
}
#Preview("Light Mode") {
WarningView(
title: "warnings.helper_permissions.title",
paragraphs: ["warnings.helper_permissions.description"],
documentationUrl: "https://nicoverbruggen.be"
)
.frame(width: 600, height: 105)
.padding(25)
}
#Preview("Dark Mode") {
WarningView(
title: "warnings.helper_permissions.title",
paragraphs: ["warnings.helper_permissions.description"],
documentationUrl: "https://nicoverbruggen.be"
)
.preferredColorScheme(.dark)
.frame(width: 600, height: 105)
.padding(25)
}

View File

@ -20,14 +20,21 @@ class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae {
prerelease: true
),
BrewPhpFormula(
name: "php@8.3",
name: "php@8.4",
displayName: "PHP 8.4",
installedVersion: nil,
upgradeVersion: "8.4.0",
prerelease: true
),
BrewPhpFormula(
name: "php",
displayName: "PHP 8.3",
installedVersion: nil,
upgradeVersion: "8.3.0",
prerelease: true
),
BrewPhpFormula(
name: "php",
name: "php@8.2",
displayName: "PHP 8.2",
installedVersion: "8.2.3",
upgradeVersion: "8.2.4"

View File

@ -0,0 +1,163 @@
//
// PhpVersionManagerView+Interactivity.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 07/11/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
import SwiftUI
extension PhpVersionManagerView {
public func runCommand(_ command: InstallAndUpgradeCommand) async {
if PhpEnvironments.shared.isBusy {
self.presentErrorAlert(
title: "phpman.action_prevented_busy.title".localized,
description: "phpman.action_prevented_busy.desc".localized,
button: "generic.ok".localized
)
return
}
do {
self.setBusyStatus(true)
try await command.execute { progress in
Task { @MainActor in
self.status.title = progress.title
self.status.description = progress.description
self.status.busy = progress.value != 1
// Whenever a key step is finished, refresh the PHP versions
if progress.value == 1 {
await self.handler.refreshPhpVersions(loadOutdated: false)
}
}
}
// Finally, after completing the command, also refresh PHP versions
await self.handler.refreshPhpVersions(loadOutdated: false)
// and mark the app as no longer busy
self.setBusyStatus(false)
} catch let error {
let error = error as! BrewCommandError
let messages = error.log.suffix(2).joined(separator: "\n")
self.setBusyStatus(false)
await self.handler.refreshPhpVersions(loadOutdated: false)
self.presentErrorAlert(
title: "phpman.failures.install.title".localized,
description: "phpman.failures.install.desc".localized(messages),
button: "generic.ok".localized
)
}
}
public func repairAll() async {
await self.runCommand(InstallAndUpgradeCommand(
title: "phpman.operations.repairing".localized,
upgrading: [],
installing: []
))
}
public func upgradeAll(_ formulae: [BrewPhpFormula]) async {
await self.runCommand(InstallAndUpgradeCommand(
title: "phpman.operations.updating".localized,
upgrading: formulae,
installing: []
))
}
public func install(_ formula: BrewPhpFormula) async {
await self.runCommand(InstallAndUpgradeCommand(
title: "phpman.operations.installing".localized(formula.displayName),
upgrading: [],
installing: [formula]
))
}
public func confirmUninstall(_ formula: BrewPhpFormula) async {
// Disallow removal of the currently active versipn
if formula.installedVersion == PhpEnvironments.shared.currentInstall?.version.text {
self.presentErrorAlert(
title: "phpman.uninstall_prevented.title".localized,
description: "phpman.uninstall_prevented.desc".localized,
button: "generic.ok".localized
)
return
}
Alert.confirm(
onWindow: App.shared.phpVersionManagerWindowController!.window!,
messageText: "phpman.warnings.removal.title".localized(formula.displayName),
informativeText: "phpman.warnings.removal.desc".localized(formula.displayName),
buttonTitle: "phpman.warnings.removal.button".localized,
buttonIsDestructive: true,
secondButtonTitle: "generic.cancel".localized,
style: .warning,
onFirstButtonPressed: {
Task { await self.uninstall(formula) }
}
)
}
public func uninstall(_ formula: BrewPhpFormula) async {
let command = RemovePhpVersionCommand(formula: formula.name)
do {
self.setBusyStatus(true)
try await command.execute { progress in
Task { @MainActor in
self.status.title = progress.title
self.status.description = progress.description
self.status.busy = progress.value != 1
if progress.value == 1 {
await self.handler.refreshPhpVersions(loadOutdated: false)
self.setBusyStatus(false)
}
}
}
} catch {
self.setBusyStatus(false)
self.presentErrorAlert(
title: "phpman.failures.uninstall.title".localized,
description: "phpman.failures.uninstall.desc".localized(
"brew uninstall \(formula.name) --force"
),
button: "generic.ok".localized
)
}
}
public func setBusyStatus(_ busy: Bool) {
Task { @MainActor in
PhpEnvironments.shared.isBusy = busy
self.status.busy = busy
}
}
public func presentErrorAlert(
title: String,
description: String,
button: String,
style: NSAlert.Style = .critical
) {
Alert.confirm(
onWindow: App.shared.phpVersionManagerWindowController!.window!,
messageText: title,
informativeText: description,
buttonTitle: button,
secondButtonTitle: "",
style: style,
onFirstButtonPressed: {}
)
}
var hasUpdates: Bool {
return self.formulae.phpVersions.contains { formula in
return formula.hasUpgrade
}
}
}

View File

@ -9,7 +9,6 @@
import Foundation
import SwiftUI
// swiftlint:disable type_body_length
struct PhpVersionManagerView: View {
@ObservedObject var formulae: BrewFormulaeObservable
@ObservedObject var status: PhpFormulaeStatus
@ -132,240 +131,133 @@ struct PhpVersionManagerView: View {
.padding(10)
}
BlockingOverlayView(busy: self.status.busy, title: self.status.title, text: self.status.description) {
List(Array(formulae.phpVersions.enumerated()), id: \.1.name) { (index, formula) in
HStack(alignment: .center, spacing: 7.0) {
Image(systemName: formula.icon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(formula.iconColor)
.padding(.horizontal, 5)
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(formula.displayName).bold()
if formula.prerelease {
Text("phpman.version.prerelease".localized.uppercased())
.font(.system(size: 9))
.padding(.horizontal, 5)
.padding(.vertical, 1)
.background(Color.appPrimary)
.foregroundColor(Color.white)
.clipShape(Capsule())
.fixedSize(horizontal: true, vertical: true)
}
}
if formula.isInstalled && formula.hasUpgrade {
Text("phpman.version.has_update".localized(
formula.installedVersion!,
formula.upgradeVersion!
))
.font(.system(size: 11))
.foregroundColor(.gray)
} else if formula.isInstalled && formula.installedVersion != nil {
Text("phpman.version.installed".localized(formula.installedVersion!))
.font(.system(size: 11))
.foregroundColor(.gray)
} else {
Text("phpman.version.available_for_installation".localizedForSwiftUI)
.font(.system(size: 11))
.foregroundColor(.gray)
}
if !formula.healthy {
Text("phpman.version.broken".localizedForSwiftUI)
.font(.system(size: 11))
.foregroundColor(.red)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if !formula.healthy {
Button("phpman.buttons.repair".localizedForSwiftUI, role: .destructive) {
Task { await self.repairAll() }
}
}
if formula.isInstalled {
Button("phpman.buttons.uninstall".localizedForSwiftUI, role: .destructive) {
Task { await self.confirmUninstall(formula) }
}
} else {
Button("phpman.buttons.install".localizedForSwiftUI) {
Task { await self.install(formula) }
}
}
BlockingOverlayView(
busy: self.status.busy,
title: self.status.title,
text: self.status.description
) {
if #available(macOS 13, *) {
List(Array(formulae.phpVersions.enumerated()), id: \.1.name) { (index, formula) in
listContent(for: formula)
.listRowBackground(
index % 2 == 0
? Color.gray.opacity(0)
: Color.gray.opacity(0.08)
)
.padding(.vertical, 8)
.padding(.horizontal, 8)
.listRowSeparator(.hidden)
}
.listRowBackground(index % 2 == 0 ? Color.gray.opacity(0): Color.gray.opacity(0.08))
.padding(.vertical, 8)
.padding(.horizontal, 8)
.edgesIgnoringSafeArea(.top)
.listStyle(PlainListStyle())
} else {
List(Array(formulae.phpVersions.enumerated()), id: \.1.name) { (index, formula) in
listContent(for: formula)
.listRowBackground(
index % 2 == 0
? Color.gray.opacity(0)
: Color.gray.opacity(0.08)
)
.padding(.vertical, 8)
.padding(.horizontal, 8)
}
.edgesIgnoringSafeArea(.top)
.listStyle(PlainListStyle())
}
.edgesIgnoringSafeArea(.top)
.listStyle(PlainListStyle())
}
}.frame(width: 600, height: 600)
}
public func runCommand(_ command: InstallAndUpgradeCommand) async {
if PhpEnvironments.shared.isBusy {
self.presentErrorAlert(
title: "phpman.action_prevented_busy.title".localized,
description: "phpman.action_prevented_busy.desc".localized,
button: "generic.ok".localized
)
return
}
// MARK: View Functions
do {
self.setBusyStatus(true)
try await command.execute { progress in
Task { @MainActor in
self.status.title = progress.title
self.status.description = progress.description
self.status.busy = progress.value != 1
private var prereleaseBadge: some View {
Text("phpman.version.prerelease".localized.uppercased())
.font(.system(size: 9))
.padding(.horizontal, 5)
.padding(.vertical, 1)
.background(Color.appPrimary)
.foregroundColor(Color.white)
.clipShape(Capsule())
.fixedSize(horizontal: true, vertical: true)
}
// Whenever a key step is finished, refresh the PHP versions
if progress.value == 1 {
await self.handler.refreshPhpVersions(loadOutdated: false)
}
private func formulaButtons(for formula: BrewPhpFormula) -> some View {
HStack {
if !formula.healthy {
Button("phpman.buttons.repair".localizedForSwiftUI, role: .destructive) {
Task { await self.repairAll() }
}
}
// Finally, after completing the command, also refresh PHP versions
await self.handler.refreshPhpVersions(loadOutdated: false)
// and mark the app as no longer busy
self.setBusyStatus(false)
} catch let error {
let error = error as! BrewCommandError
let messages = error.log.suffix(2).joined(separator: "\n")
self.setBusyStatus(false)
await self.handler.refreshPhpVersions(loadOutdated: false)
self.presentErrorAlert(
title: "phpman.failures.install.title".localized,
description: "phpman.failures.install.desc".localized(messages),
button: "generic.ok".localized
)
}
}
public func repairAll() async {
await self.runCommand(InstallAndUpgradeCommand(
title: "phpman.operations.repairing".localized,
upgrading: [],
installing: []
))
}
public func upgradeAll(_ formulae: [BrewPhpFormula]) async {
await self.runCommand(InstallAndUpgradeCommand(
title: "phpman.operations.updating".localized,
upgrading: formulae,
installing: []
))
}
public func install(_ formula: BrewPhpFormula) async {
await self.runCommand(InstallAndUpgradeCommand(
title: "phpman.operations.installing".localized(formula.displayName),
upgrading: [],
installing: [formula]
))
}
public func confirmUninstall(_ formula: BrewPhpFormula) async {
// Disallow removal of the currently active versipn
if formula.installedVersion == PhpEnvironments.shared.currentInstall?.version.text {
self.presentErrorAlert(
title: "phpman.uninstall_prevented.title".localized,
description: "phpman.uninstall_prevented.desc".localized,
button: "generic.ok".localized
)
return
}
Alert.confirm(
onWindow: App.shared.phpVersionManagerWindowController!.window!,
messageText: "phpman.warnings.removal.title".localized(formula.displayName),
informativeText: "phpman.warnings.removal.desc".localized(formula.displayName),
buttonTitle: "phpman.warnings.removal.button".localized,
buttonIsDestructive: true,
secondButtonTitle: "generic.cancel".localized,
style: .warning,
onFirstButtonPressed: {
Task { await self.uninstall(formula) }
}
)
}
public func uninstall(_ formula: BrewPhpFormula) async {
let command = RemovePhpVersionCommand(formula: formula.name)
do {
self.setBusyStatus(true)
try await command.execute { progress in
Task { @MainActor in
self.status.title = progress.title
self.status.description = progress.description
self.status.busy = progress.value != 1
if progress.value == 1 {
await self.handler.refreshPhpVersions(loadOutdated: false)
self.setBusyStatus(false)
}
if formula.isInstalled {
Button("phpman.buttons.uninstall".localizedForSwiftUI, role: .destructive) {
Task { await self.confirmUninstall(formula) }
}
} else {
Button("phpman.buttons.install".localizedForSwiftUI) {
Task { await self.install(formula) }
}
}
} catch {
self.setBusyStatus(false)
self.presentErrorAlert(
title: "phpman.failures.uninstall.title".localized,
description: "phpman.failures.uninstall.desc".localized(
"brew uninstall \(formula.name) --force"
),
button: "generic.ok".localized
)
}
}
public func setBusyStatus(_ busy: Bool) {
Task { @MainActor in
PhpEnvironments.shared.isBusy = busy
self.status.busy = busy
private func formulaDescription(for formula: BrewPhpFormula) -> some View {
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(formula.displayName).bold()
if formula.prerelease {
prereleaseBadge
}
}
if formula.isInstalled && formula.hasUpgrade {
Text("phpman.version.has_update".localized(
formula.installedVersion!,
formula.upgradeVersion!
))
.font(.system(size: 11))
.foregroundColor(.gray)
} else if formula.isInstalled && formula.installedVersion != nil {
Text("phpman.version.installed".localized(formula.installedVersion!))
.font(.system(size: 11))
.foregroundColor(.gray)
} else {
Text("phpman.version.available_for_installation".localizedForSwiftUI)
.font(.system(size: 11))
.foregroundColor(.gray)
}
if !formula.healthy {
Text("phpman.version.broken".localizedForSwiftUI)
.font(.system(size: 11))
.foregroundColor(.red)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
public func presentErrorAlert(
title: String,
description: String,
button: String,
style: NSAlert.Style = .critical
) {
Alert.confirm(
onWindow: App.shared.phpVersionManagerWindowController!.window!,
messageText: title,
informativeText: description,
buttonTitle: button,
secondButtonTitle: "",
style: style,
onFirstButtonPressed: {}
)
private func formulaIcon(for formula: BrewPhpFormula) -> some View {
Image(systemName: formula.icon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(formula.iconColor)
.padding(.horizontal, 5)
}
var hasUpdates: Bool {
return self.formulae.phpVersions.contains { formula in
return formula.hasUpgrade
private func listContent(for formula: BrewPhpFormula) -> some View {
HStack(alignment: .center, spacing: 7.0) {
formulaIcon(for: formula)
formulaDescription(for: formula)
formulaButtons(for: formula)
}
}
}
// swiftlint:enable type_body_length
struct PhpVersionManagerView_Previews: PreviewProvider {
static var previews: some View {
PhpVersionManagerView(
formulae: Brew.shared.formulae,
handler: FakeBrewFormulaeHandler()
).frame(width: 600, height: 600)
}
#Preview {
PhpVersionManagerView(
formulae: Brew.shared.formulae,
handler: FakeBrewFormulaeHandler()
).frame(width: 600, height: 600)
}