mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-07 03:50:08 +02:00
🏗 WIP: Functional service toggling
This commit is contained in:
@ -10,13 +10,13 @@ import Foundation
|
||||
|
||||
class FakeServicesManager: ServicesManager {
|
||||
var fixedFormulae: [String] = []
|
||||
var fixedStatus: ServiceStatus = .loading
|
||||
var fixedStatus: ServiceStatus = .active
|
||||
|
||||
override init() {}
|
||||
|
||||
init(
|
||||
formulae: [String] = ["php", "nginx", "dnsmasq"],
|
||||
status: ServiceStatus = .loading
|
||||
status: ServiceStatus = .active
|
||||
) {
|
||||
super.init()
|
||||
|
||||
@ -27,9 +27,10 @@ class FakeServicesManager: ServicesManager {
|
||||
self.fixedStatus = status
|
||||
|
||||
self.serviceWrappers = self.formulae.map {
|
||||
let wrapper = ServiceWrapper(formula: $0)
|
||||
wrapper.isBusy = (status == .loading)
|
||||
wrapper.service = HomebrewService.dummy(named: $0.name, enabled: true)
|
||||
let wrapper = ServiceWrapper(
|
||||
formula: $0,
|
||||
service: HomebrewService.dummy(named: $0.name, enabled: true)
|
||||
)
|
||||
return wrapper
|
||||
}
|
||||
}
|
||||
@ -42,35 +43,9 @@ class FakeServicesManager: ServicesManager {
|
||||
|
||||
override func reloadServicesStatus() async {
|
||||
await delay(seconds: 0.3)
|
||||
|
||||
for formula in self.serviceWrappers {
|
||||
formula.service?.running = true
|
||||
formula.isBusy = false
|
||||
}
|
||||
|
||||
broadcastServicesUpdated()
|
||||
}
|
||||
|
||||
override func toggleService(named: String) async {
|
||||
guard let wrapper = self[named] else {
|
||||
return Log.err("The wrapper for service \(named) does not exist.")
|
||||
}
|
||||
|
||||
wrapper.isBusy = true
|
||||
Task { @MainActor in
|
||||
wrapper.objectWillChange.send()
|
||||
}
|
||||
|
||||
guard let service = wrapper.service else {
|
||||
return Log.err("The actual service for wrapper \(named) is nil.")
|
||||
}
|
||||
|
||||
await delay(seconds: 2)
|
||||
service.running = !service.running
|
||||
wrapper.isBusy = false
|
||||
Task { @MainActor in
|
||||
wrapper.objectWillChange.send()
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
await delay(seconds: 0.3)
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import Foundation
|
||||
public enum ServiceStatus: String {
|
||||
case active
|
||||
case inactive
|
||||
case loading
|
||||
case missing
|
||||
}
|
||||
|
||||
@ -22,40 +21,28 @@ public enum ServiceStatus: String {
|
||||
Service wrapper, that contains the Homebrew JSON output (if determined) and the formula.
|
||||
This helps the app determine whether a service should run as an administrator or not.
|
||||
*/
|
||||
public class ServiceWrapper: ObservableObject, Identifiable, Hashable {
|
||||
public struct ServiceWrapper: Hashable {
|
||||
var formula: HomebrewFormula
|
||||
var service: HomebrewService?
|
||||
|
||||
var isBusy: Bool = false
|
||||
var status: ServiceStatus = .missing
|
||||
|
||||
public var name: String {
|
||||
return formula.name
|
||||
}
|
||||
|
||||
public var status: ServiceStatus {
|
||||
if isBusy {
|
||||
return .loading
|
||||
}
|
||||
|
||||
guard let service = self.service else {
|
||||
return .missing
|
||||
}
|
||||
|
||||
return service.running ? .active : .inactive
|
||||
}
|
||||
|
||||
init(formula: HomebrewFormula) {
|
||||
init(formula: HomebrewFormula, service: HomebrewService? = nil) {
|
||||
self.formula = formula
|
||||
self.isBusy = true
|
||||
|
||||
if service != nil {
|
||||
self.status = service!.running ? .active : .inactive
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: ServiceWrapper, rhs: ServiceWrapper) -> Bool {
|
||||
return lhs.service == rhs.service
|
||||
&& lhs.formula == rhs.formula
|
||||
return lhs.hashValue == rhs.hashValue
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(formula)
|
||||
hasher.combine(service)
|
||||
hasher.combine(status)
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ class ServicesManager: ObservableObject {
|
||||
public static func useFake() {
|
||||
ServicesManager.shared = FakeServicesManager.init(
|
||||
formulae: ["php", "nginx", "dnsmasq", "mysql"],
|
||||
status: .loading
|
||||
status: .active
|
||||
)
|
||||
}
|
||||
|
||||
@ -39,9 +39,6 @@ class ServicesManager: ObservableObject {
|
||||
|
||||
let statuses = self.serviceWrappers[0...2].map { $0.status }
|
||||
|
||||
if statuses.contains(.loading) {
|
||||
return "Determining Valet status..."
|
||||
}
|
||||
if statuses.contains(.missing) {
|
||||
return "A key service is not installed."
|
||||
}
|
||||
@ -58,9 +55,6 @@ class ServicesManager: ObservableObject {
|
||||
}
|
||||
|
||||
let statuses = self.serviceWrappers[0...2].map { $0.status }
|
||||
if statuses.contains(.loading) {
|
||||
return .orange
|
||||
}
|
||||
if statuses.contains(.missing) {
|
||||
return .red
|
||||
}
|
||||
@ -92,10 +86,6 @@ class ServicesManager: ObservableObject {
|
||||
*/
|
||||
public func broadcastServicesUpdated() {
|
||||
Task { @MainActor in
|
||||
self.serviceWrappers.forEach { wrapper in
|
||||
wrapper.objectWillChange.send()
|
||||
}
|
||||
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,11 @@ class ValetServicesManager: ServicesManager {
|
||||
Task { await self.reloadServicesStatus() }
|
||||
}
|
||||
|
||||
/**
|
||||
The last known state of all Homebrew services.
|
||||
*/
|
||||
var homebrewServices: [HomebrewService] = []
|
||||
|
||||
/**
|
||||
This method allows us to reload the Homebrew services, but we run this command
|
||||
twice (once for user services, and once for root services). Please note that
|
||||
@ -53,20 +58,23 @@ class ValetServicesManager: ServicesManager {
|
||||
.filter({ return userServiceNames.contains($0.name) })
|
||||
}
|
||||
|
||||
// Ensure both commands complete (but run concurrently)
|
||||
for await services in group {
|
||||
// For both groups (user and root services), set the service to the wrapper object
|
||||
for service in services {
|
||||
self[service.name]?.service = service
|
||||
}
|
||||
self.homebrewServices = []
|
||||
|
||||
for wrapper in serviceWrappers {
|
||||
wrapper.isBusy = false
|
||||
}
|
||||
for await services in group {
|
||||
homebrewServices.append(contentsOf: services)
|
||||
}
|
||||
|
||||
// Broadcast that all services have been updated
|
||||
self.broadcastServicesUpdated()
|
||||
Task { @MainActor in
|
||||
// Ensure both commands complete (but run concurrently)
|
||||
serviceWrappers = formulae.map { formula in
|
||||
ServiceWrapper(formula: formula, service: homebrewServices.first(where: { service in
|
||||
service.name == formula.name
|
||||
}))
|
||||
}
|
||||
|
||||
// Broadcast that all services have been updated
|
||||
self.broadcastServicesUpdated()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -75,12 +83,8 @@ class ValetServicesManager: ServicesManager {
|
||||
return Log.err("The wrapper for '\(named)' is missing.")
|
||||
}
|
||||
|
||||
if wrapper.service == nil {
|
||||
return Log.err("The Homebrew service for \(named) is missing.")
|
||||
}
|
||||
|
||||
// Prepare the appropriate command to stop or start a service
|
||||
let action = wrapper.service!.running ? "stop" : "start"
|
||||
let action = wrapper.status == .active ? "stop" : "start"
|
||||
let command = "services \(action) \(wrapper.formula.name)"
|
||||
|
||||
// Run the command
|
||||
|
@ -57,7 +57,7 @@ struct ServicesView: View {
|
||||
VStack(alignment: .leading, spacing: CGFloat(self.rowSpacing)) {
|
||||
ForEach(manager.serviceWrappers.chunked(by: perRow), id: \.self) { chunk in
|
||||
HStack {
|
||||
ForEach(chunk) { service in
|
||||
ForEach(chunk, id: \.self) { service in
|
||||
ServiceView(service: service)
|
||||
.frame(minWidth: 70)
|
||||
}
|
||||
@ -86,7 +86,8 @@ struct ServicesView: View {
|
||||
}
|
||||
|
||||
struct ServiceView: View {
|
||||
@ObservedObject var service: ServiceWrapper
|
||||
var service: ServiceWrapper
|
||||
@State var isBusy: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
@ -95,67 +96,58 @@ struct ServiceView: View {
|
||||
.frame(minWidth: 70, alignment: .center)
|
||||
.padding(.top, 4)
|
||||
.padding(.bottom, 2)
|
||||
if service.status == .loading {
|
||||
if isBusy {
|
||||
ProgressView()
|
||||
.scaleEffect(x: 0.4, y: 0.4, anchor: .center)
|
||||
.frame(minWidth: 70, alignment: .center)
|
||||
.frame(width: 25, height: 25)
|
||||
}
|
||||
if service.status == .missing {
|
||||
Button {
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
title: "alert.warnings.service_missing.title".localized,
|
||||
subtitle: "alert.warnings.service_missing.subtitle".localized,
|
||||
description: "alert.warnings.service_missing.description".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
} else {
|
||||
if service.status == .missing {
|
||||
Button {
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
title: "alert.warnings.service_missing.title".localized,
|
||||
subtitle: "alert.warnings.service_missing.subtitle".localized,
|
||||
description: "alert.warnings.service_missing.description".localized
|
||||
)
|
||||
.withPrimary(text: "OK")
|
||||
.show()
|
||||
}
|
||||
} label: {
|
||||
Text("?")
|
||||
}
|
||||
} label: {
|
||||
Text("?")
|
||||
.focusable(false)
|
||||
// .buttonStyle(BlueButton())
|
||||
.frame(minWidth: 70, alignment: .center)
|
||||
}
|
||||
if service.status == .active || service.status == .inactive {
|
||||
Button {
|
||||
Task {
|
||||
isBusy = true
|
||||
await ServicesManager.shared.toggleService(named: service.name)
|
||||
isBusy = false
|
||||
}
|
||||
} label: {
|
||||
Image(
|
||||
systemName: service.status == .active ? "checkmark" : "xmark"
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 12.0, height: 12.0)
|
||||
.foregroundColor(
|
||||
service.status == .active ? Color("IconColorGreen") : Color("IconColorRed")
|
||||
)
|
||||
}.frame(width: 25, height: 25)
|
||||
}
|
||||
.focusable(false)
|
||||
// .buttonStyle(BlueButton())
|
||||
.frame(minWidth: 70, alignment: .center)
|
||||
}
|
||||
if service.status == .active || service.status == .inactive {
|
||||
Button {
|
||||
Task { await ServicesManager.shared.toggleService(named: service.name) }
|
||||
} label: {
|
||||
Image(
|
||||
systemName: service.status == .active ? "checkmark" : "xmark"
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 12.0, height: 12.0)
|
||||
.foregroundColor(
|
||||
service.status == .active ? Color("IconColorGreen") : Color("IconColorRed")
|
||||
)
|
||||
}.frame(width: 25, height: 25)
|
||||
}
|
||||
}.frame(minWidth: 70)
|
||||
}
|
||||
}
|
||||
|
||||
public struct BlueButton: ButtonStyle {
|
||||
public func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.frame(width: 25, height: 25)
|
||||
.background(configuration.isPressed
|
||||
? Color(red: 0, green: 0.5, blue: 0.9)
|
||||
: Color(red: 0, green: 0, blue: 0.5)
|
||||
)
|
||||
.foregroundColor(.white)
|
||||
.clipShape(Capsule())
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
struct ServicesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ServicesView(manager: FakeServicesManager(
|
||||
formulae: ["php", "nginx", "dnsmasq"],
|
||||
status: .loading
|
||||
status: .active
|
||||
), perRow: 4)
|
||||
.frame(width: 330.0)
|
||||
.previewDisplayName("Loading")
|
||||
|
Reference in New Issue
Block a user