mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2026-03-25 13:40:08 +01:00
🍱 Improve appearance of alerts
This commit is contained in:
@@ -11,14 +11,16 @@ import SwiftUI
|
||||
struct MarkdownTextView: View {
|
||||
let string: String
|
||||
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.fontSize = fontSize
|
||||
self.textColor = textColor
|
||||
}
|
||||
|
||||
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 {
|
||||
let string: String
|
||||
let fontSize: CGFloat
|
||||
let textColor: NSColor
|
||||
|
||||
// MARK: - Static Properties
|
||||
|
||||
@@ -44,14 +45,15 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
||||
|
||||
func updateNSView(_ textView: CodeBlockTextView, context: Context) {
|
||||
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)
|
||||
}
|
||||
|
||||
private func configure(_ textView: CodeBlockTextView, coordinator: Coordinator) {
|
||||
coordinator.lastString = string
|
||||
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.invalidateIntrinsicContentSize()
|
||||
}
|
||||
@@ -59,11 +61,12 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
||||
class Coordinator {
|
||||
var lastString: String?
|
||||
var lastFontSize: CGFloat?
|
||||
var lastTextColor: NSColor?
|
||||
}
|
||||
|
||||
// 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 font = NSFont.systemFont(ofSize: fontSize)
|
||||
|
||||
@@ -73,7 +76,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
||||
|
||||
let defaultAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: font,
|
||||
.foregroundColor: NSColor.labelColor,
|
||||
.foregroundColor: textColor,
|
||||
.paragraphStyle: paragraphStyle
|
||||
]
|
||||
|
||||
@@ -85,8 +88,8 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
||||
|
||||
// Collect code span ranges once for bold and italic passes
|
||||
let codeRanges = codeSpanRanges(in: result)
|
||||
handleBoldMarkup(in: result, fontSize: fontSize, paragraphStyle: paragraphStyle, codeRanges: codeRanges)
|
||||
handleItalicMarkup(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, textColor: textColor, codeRanges: codeRanges)
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -156,6 +159,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
||||
in result: NSMutableAttributedString,
|
||||
fontSize: CGFloat,
|
||||
paragraphStyle: NSParagraphStyle,
|
||||
textColor: NSColor,
|
||||
codeRanges: [NSRange]
|
||||
) {
|
||||
let boldFont = NSFont.boldSystemFont(ofSize: fontSize)
|
||||
@@ -168,7 +172,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
||||
string: innerText,
|
||||
attributes: [
|
||||
.font: boldFont,
|
||||
.foregroundColor: NSColor.labelColor,
|
||||
.foregroundColor: textColor,
|
||||
.paragraphStyle: paragraphStyle
|
||||
]
|
||||
)
|
||||
@@ -182,6 +186,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
||||
in result: NSMutableAttributedString,
|
||||
fontSize: CGFloat,
|
||||
paragraphStyle: NSParagraphStyle,
|
||||
textColor: NSColor,
|
||||
codeRanges: [NSRange]
|
||||
) {
|
||||
let italicFont = NSFontManager.shared.convert(
|
||||
@@ -197,7 +202,7 @@ struct MarkdownTextViewRepresentable: NSViewRepresentable {
|
||||
string: innerText,
|
||||
attributes: [
|
||||
.font: italicFont,
|
||||
.foregroundColor: NSColor.labelColor,
|
||||
.foregroundColor: textColor,
|
||||
.paragraphStyle: paragraphStyle
|
||||
]
|
||||
)
|
||||
|
||||
@@ -13,24 +13,15 @@ struct StartupAlertHeaderView: View {
|
||||
let subtitleText: String
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(nsImage: NSApp.applicationIconImage)
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(titleText)
|
||||
.font(.system(size: 15, weight: .bold, design: .rounded))
|
||||
.textSelection(.enabled)
|
||||
.padding(.bottom, 2)
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(titleText)
|
||||
.font(.system(size: 15, weight: .bold))
|
||||
.textSelection(.enabled)
|
||||
|
||||
MarkdownTextView(subtitleText, fontSize: 13)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
MarkdownTextView(subtitleText, fontSize: 12)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(15)
|
||||
.padding(.top, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,39 +13,48 @@ struct StartupAlertView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
StartupAlertHeaderView(
|
||||
titleText: viewModel.check.titleText,
|
||||
subtitleText: viewModel.check.subtitleText
|
||||
)
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Image(nsImage: NSApp.applicationIconImage)
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
// Fix command description: only shown in idle state when a fix is available
|
||||
if viewModel.state == .idle && viewModel.hasFix {
|
||||
StartupFixCommandView(
|
||||
command: viewModel.check.fixDescription ?? ""
|
||||
)
|
||||
.padding(.horizontal, 10).padding(.leading, 72)
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
StartupAlertHeaderView(
|
||||
titleText: viewModel.check.titleText,
|
||||
subtitleText: viewModel.check.subtitleText
|
||||
)
|
||||
.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)
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
.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)
|
||||
}
|
||||
.padding(15)
|
||||
.padding(.top, -5)
|
||||
|
||||
Divider()
|
||||
|
||||
@@ -133,7 +142,7 @@ struct StartupAlertView: View {
|
||||
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: "", 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: [
|
||||
OutputLine(text: "==> Linking Binary 'php' to '/opt/homebrew/bin/php'", 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. */
|
||||
@MainActor private func pass() {
|
||||
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. */
|
||||
@MainActor private func fail() {
|
||||
self.state = .failed
|
||||
self.appendOutput("---\n\("startup.fix.not_resolved".localized)", .stdErr)
|
||||
self.appendOutput("\n\("startup.fix.not_resolved".localized)", .stdErr)
|
||||
}
|
||||
|
||||
/** An error occurred. */
|
||||
@MainActor private func errorAndIdle(_ error: Error) {
|
||||
self.state = .failed
|
||||
self.appendOutput("---\nError: \(error.localizedDescription)", .stdErr)
|
||||
self.appendOutput("\nError: \(error.localizedDescription)", .stdErr)
|
||||
}
|
||||
|
||||
// MARK: - Alert Outcomes
|
||||
|
||||
Reference in New Issue
Block a user