mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2026-03-27 06:20:08 +01:00
🍱 Improve appearance of alerts
This commit is contained in:
@@ -11,14 +11,16 @@ import SwiftUI
|
|||||||
struct MarkdownTextView: View {
|
struct MarkdownTextView: View {
|
||||||
let string: String
|
let string: String
|
||||||
let fontSize: CGFloat
|
let fontSize: CGFloat
|
||||||
|
let textColor: NSColor
|
||||||
|
|
||||||
init(_ string: String, fontSize: CGFloat = 12) {
|
init(_ string: String, fontSize: CGFloat = 12, textColor: NSColor = .labelColor) {
|
||||||
self.string = string
|
self.string = string
|
||||||
self.fontSize = fontSize
|
self.fontSize = fontSize
|
||||||
|
self.textColor = textColor
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
MarkdownTextViewRepresentable(string: string, fontSize: fontSize)
|
MarkdownTextViewRepresentable(string: string, fontSize: fontSize, textColor: textColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import AppKit
|
|||||||
struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
||||||
let string: String
|
let string: String
|
||||||
let fontSize: CGFloat
|
let fontSize: CGFloat
|
||||||
|
let textColor: NSColor
|
||||||
|
|
||||||
// MARK: - Static Properties
|
// MARK: - Static Properties
|
||||||
|
|
||||||
@@ -44,14 +45,15 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
|||||||
|
|
||||||
func updateNSView(_ textView: CodeBlockTextView, context: Context) {
|
func updateNSView(_ textView: CodeBlockTextView, context: Context) {
|
||||||
let coordinator = context.coordinator
|
let coordinator = context.coordinator
|
||||||
guard string != coordinator.lastString || fontSize != coordinator.lastFontSize else { return }
|
guard string != coordinator.lastString || fontSize != coordinator.lastFontSize || textColor != coordinator.lastTextColor else { return }
|
||||||
configure(textView, coordinator: coordinator)
|
configure(textView, coordinator: coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configure(_ textView: CodeBlockTextView, coordinator: Coordinator) {
|
private func configure(_ textView: CodeBlockTextView, coordinator: Coordinator) {
|
||||||
coordinator.lastString = string
|
coordinator.lastString = string
|
||||||
coordinator.lastFontSize = fontSize
|
coordinator.lastFontSize = fontSize
|
||||||
let attributed = Self.buildAttributedString(from: string, fontSize: fontSize)
|
coordinator.lastTextColor = textColor
|
||||||
|
let attributed = Self.buildAttributedString(from: string, fontSize: fontSize, textColor: textColor)
|
||||||
textView.textStorage?.setAttributedString(attributed)
|
textView.textStorage?.setAttributedString(attributed)
|
||||||
textView.invalidateIntrinsicContentSize()
|
textView.invalidateIntrinsicContentSize()
|
||||||
}
|
}
|
||||||
@@ -59,11 +61,12 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
|||||||
class Coordinator {
|
class Coordinator {
|
||||||
var lastString: String?
|
var lastString: String?
|
||||||
var lastFontSize: CGFloat?
|
var lastFontSize: CGFloat?
|
||||||
|
var lastTextColor: NSColor?
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Attributed String Builder
|
// MARK: - Attributed String Builder
|
||||||
|
|
||||||
static func buildAttributedString(from string: String, fontSize: CGFloat) -> NSAttributedString {
|
static func buildAttributedString(from string: String, fontSize: CGFloat, textColor: NSColor = .labelColor) -> NSAttributedString {
|
||||||
let result = NSMutableAttributedString()
|
let result = NSMutableAttributedString()
|
||||||
let font = NSFont.systemFont(ofSize: fontSize)
|
let font = NSFont.systemFont(ofSize: fontSize)
|
||||||
|
|
||||||
@@ -73,7 +76,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
|||||||
|
|
||||||
let defaultAttributes: [NSAttributedString.Key: Any] = [
|
let defaultAttributes: [NSAttributedString.Key: Any] = [
|
||||||
.font: font,
|
.font: font,
|
||||||
.foregroundColor: NSColor.labelColor,
|
.foregroundColor: textColor,
|
||||||
.paragraphStyle: paragraphStyle
|
.paragraphStyle: paragraphStyle
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -85,8 +88,8 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
|||||||
|
|
||||||
// Collect code span ranges once for bold and italic passes
|
// Collect code span ranges once for bold and italic passes
|
||||||
let codeRanges = codeSpanRanges(in: result)
|
let codeRanges = codeSpanRanges(in: result)
|
||||||
handleBoldMarkup(in: result, fontSize: fontSize, paragraphStyle: paragraphStyle, codeRanges: codeRanges)
|
handleBoldMarkup(in: result, fontSize: fontSize, paragraphStyle: paragraphStyle, textColor: textColor, codeRanges: codeRanges)
|
||||||
handleItalicMarkup(in: result, fontSize: fontSize, paragraphStyle: paragraphStyle, codeRanges: codeRanges)
|
handleItalicMarkup(in: result, fontSize: fontSize, paragraphStyle: paragraphStyle, textColor: textColor, codeRanges: codeRanges)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -156,6 +159,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
|||||||
in result: NSMutableAttributedString,
|
in result: NSMutableAttributedString,
|
||||||
fontSize: CGFloat,
|
fontSize: CGFloat,
|
||||||
paragraphStyle: NSParagraphStyle,
|
paragraphStyle: NSParagraphStyle,
|
||||||
|
textColor: NSColor,
|
||||||
codeRanges: [NSRange]
|
codeRanges: [NSRange]
|
||||||
) {
|
) {
|
||||||
let boldFont = NSFont.boldSystemFont(ofSize: fontSize)
|
let boldFont = NSFont.boldSystemFont(ofSize: fontSize)
|
||||||
@@ -168,7 +172,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
|||||||
string: innerText,
|
string: innerText,
|
||||||
attributes: [
|
attributes: [
|
||||||
.font: boldFont,
|
.font: boldFont,
|
||||||
.foregroundColor: NSColor.labelColor,
|
.foregroundColor: textColor,
|
||||||
.paragraphStyle: paragraphStyle
|
.paragraphStyle: paragraphStyle
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -182,6 +186,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
|||||||
in result: NSMutableAttributedString,
|
in result: NSMutableAttributedString,
|
||||||
fontSize: CGFloat,
|
fontSize: CGFloat,
|
||||||
paragraphStyle: NSParagraphStyle,
|
paragraphStyle: NSParagraphStyle,
|
||||||
|
textColor: NSColor,
|
||||||
codeRanges: [NSRange]
|
codeRanges: [NSRange]
|
||||||
) {
|
) {
|
||||||
let italicFont = NSFontManager.shared.convert(
|
let italicFont = NSFontManager.shared.convert(
|
||||||
@@ -197,7 +202,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
|||||||
string: innerText,
|
string: innerText,
|
||||||
attributes: [
|
attributes: [
|
||||||
.font: italicFont,
|
.font: italicFont,
|
||||||
.foregroundColor: NSColor.labelColor,
|
.foregroundColor: textColor,
|
||||||
.paragraphStyle: paragraphStyle
|
.paragraphStyle: paragraphStyle
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,24 +13,15 @@ struct StartupAlertHeaderView: View {
|
|||||||
let subtitleText: String
|
let subtitleText: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Image(nsImage: NSApp.applicationIconImage)
|
Text(titleText)
|
||||||
.resizable()
|
.font(.system(size: 15, weight: .bold, design: .rounded))
|
||||||
.frame(width: 60, height: 60)
|
.textSelection(.enabled)
|
||||||
|
.padding(.bottom, 2)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
MarkdownTextView(subtitleText, fontSize: 12)
|
||||||
Text(titleText)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.font(.system(size: 15, weight: .bold))
|
|
||||||
.textSelection(.enabled)
|
|
||||||
|
|
||||||
MarkdownTextView(subtitleText, fontSize: 13)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.padding(15)
|
|
||||||
.padding(.top, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,39 +13,48 @@ struct StartupAlertView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
StartupAlertHeaderView(
|
HStack(alignment: .top, spacing: 12) {
|
||||||
titleText: viewModel.check.titleText,
|
Image(nsImage: NSApp.applicationIconImage)
|
||||||
subtitleText: viewModel.check.subtitleText
|
.resizable()
|
||||||
)
|
.frame(width: 60, height: 60)
|
||||||
|
|
||||||
// Fix command description: only shown in idle state when a fix is available
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
if viewModel.state == .idle && viewModel.hasFix {
|
StartupAlertHeaderView(
|
||||||
StartupFixCommandView(
|
titleText: viewModel.check.titleText,
|
||||||
command: viewModel.check.fixDescription ?? ""
|
subtitleText: viewModel.check.subtitleText
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 10).padding(.leading, 72)
|
.padding(.top, 5)
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
|
||||||
|
// Fix command description: only shown in idle state when a fix is available
|
||||||
|
if viewModel.state == .idle && viewModel.hasFix {
|
||||||
|
StartupFixCommandView(
|
||||||
|
command: viewModel.check.fixDescription ?? ""
|
||||||
|
)
|
||||||
|
} else if viewModel.state == .idle && !viewModel.hasFix {
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal output: shown during and after fix execution
|
||||||
|
if !viewModel.outputLines.isEmpty
|
||||||
|
&& (viewModel.state == .running || viewModel.state == .completed || viewModel.state == .failed) {
|
||||||
|
StartupOutputView(
|
||||||
|
lines: viewModel.outputLines,
|
||||||
|
isRunning: viewModel.state == .running
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description text: shown in idle state
|
||||||
|
if !viewModel.check.descriptionText.isEmpty && viewModel.state == .idle {
|
||||||
|
MarkdownTextView(viewModel.check.descriptionText, fontSize: 12, textColor: NSColor.secondaryLabelColor)
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.trailing, 10)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
.padding(15)
|
||||||
// Terminal output: shown during and after fix execution
|
.padding(.top, -5)
|
||||||
if !viewModel.outputLines.isEmpty
|
|
||||||
&& (viewModel.state == .running || viewModel.state == .completed || viewModel.state == .failed) {
|
|
||||||
StartupOutputView(
|
|
||||||
lines: viewModel.outputLines,
|
|
||||||
isRunning: viewModel.state == .running
|
|
||||||
)
|
|
||||||
.padding(15).padding(.leading, 72)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description text: shown in idle state
|
|
||||||
if !viewModel.check.descriptionText.isEmpty && viewModel.state == .idle {
|
|
||||||
MarkdownTextView(viewModel.check.descriptionText, fontSize: 12)
|
|
||||||
.padding(.vertical, 15)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.leading, 64)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
@@ -133,7 +142,7 @@ struct StartupAlertView: View {
|
|||||||
OutputLine(text: "==> Linking Binary 'php' to '/opt/homebrew/bin/php'", stream: .stdOut),
|
OutputLine(text: "==> Linking Binary 'php' to '/opt/homebrew/bin/php'", stream: .stdOut),
|
||||||
OutputLine(text: "Warning: php is keg-only and must be linked with --force", stream: .stdErr),
|
OutputLine(text: "Warning: php is keg-only and must be linked with --force", stream: .stdErr),
|
||||||
OutputLine(text: "", stream: .stdOut),
|
OutputLine(text: "", stream: .stdOut),
|
||||||
OutputLine(text: "---\nFix did not resolve the issue.", stream: .stdOut)
|
OutputLine(text: "\nFix did not resolve the issue.", stream: .stdOut)
|
||||||
]
|
]
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -155,7 +164,7 @@ struct StartupAlertView: View {
|
|||||||
outputLines: [
|
outputLines: [
|
||||||
OutputLine(text: "==> Linking Binary 'php' to '/opt/homebrew/bin/php'", stream: .stdOut),
|
OutputLine(text: "==> Linking Binary 'php' to '/opt/homebrew/bin/php'", stream: .stdOut),
|
||||||
OutputLine(text: "==> Linking php... linked 25 files", stream: .stdOut),
|
OutputLine(text: "==> Linking php... linked 25 files", stream: .stdOut),
|
||||||
OutputLine(text: "---\nFix applied successfully! Continuing...", stream: .stdOut)
|
OutputLine(text: "\nFix applied successfully! Continuing...", stream: .stdOut)
|
||||||
]
|
]
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,19 +101,19 @@ class StartupAlertViewModel: ObservableObject {
|
|||||||
/** Marks the current fix as completed, with success. */
|
/** Marks the current fix as completed, with success. */
|
||||||
@MainActor private func pass() {
|
@MainActor private func pass() {
|
||||||
self.state = .completed
|
self.state = .completed
|
||||||
self.appendOutput("---\n\("startup.fix.applied".localized)", .stdOut)
|
self.appendOutput("\n\("startup.fix.applied".localized)", .stdOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Marks the current fix as completed, with failure. */
|
/** Marks the current fix as completed, with failure. */
|
||||||
@MainActor private func fail() {
|
@MainActor private func fail() {
|
||||||
self.state = .failed
|
self.state = .failed
|
||||||
self.appendOutput("---\n\("startup.fix.not_resolved".localized)", .stdErr)
|
self.appendOutput("\n\("startup.fix.not_resolved".localized)", .stdErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An error occurred. */
|
/** An error occurred. */
|
||||||
@MainActor private func errorAndIdle(_ error: Error) {
|
@MainActor private func errorAndIdle(_ error: Error) {
|
||||||
self.state = .failed
|
self.state = .failed
|
||||||
self.appendOutput("---\nError: \(error.localizedDescription)", .stdErr)
|
self.appendOutput("\nError: \(error.localizedDescription)", .stdErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Alert Outcomes
|
// MARK: - Alert Outcomes
|
||||||
|
|||||||
Reference in New Issue
Block a user