refactor(macos): refresh skills settings layout
This commit is contained in:
@@ -9,17 +9,12 @@ struct SkillsSettings: View {
|
|||||||
@State private var filter: SkillsFilter = .all
|
@State private var filter: SkillsFilter = .all
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
VStack(alignment: .leading, spacing: 14) {
|
self.header
|
||||||
self.header
|
self.filterBar
|
||||||
self.filterBar
|
self.statusBanner
|
||||||
self.statusBanner
|
self.skillsList
|
||||||
self.skillsList
|
Spacer(minLength: 0)
|
||||||
Spacer(minLength: 0)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal, 24)
|
|
||||||
.padding(.vertical, 18)
|
|
||||||
}
|
}
|
||||||
.task { await self.model.refresh() }
|
.task { await self.model.refresh() }
|
||||||
.sheet(item: self.$envEditor) { editor in
|
.sheet(item: self.$envEditor) { editor in
|
||||||
@@ -36,17 +31,26 @@ struct SkillsSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var header: some View {
|
private var header: some View {
|
||||||
HStack(alignment: .top, spacing: 12) {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Skills")
|
Text("Skills")
|
||||||
.font(.title3.weight(.semibold))
|
.font(.headline)
|
||||||
Text("Skills are enabled when requirements are met (binaries, env, config).")
|
Text("Skills are enabled when requirements are met (binaries, env, config).")
|
||||||
.font(.callout)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Refresh") { Task { await self.model.refresh() } }
|
if self.model.isLoading {
|
||||||
.disabled(self.model.isLoading)
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Button {
|
||||||
|
Task { await self.model.refresh() }
|
||||||
|
} label: {
|
||||||
|
Label("Refresh", systemImage: "arrow.clockwise")
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.help("Refresh")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,49 +67,59 @@ struct SkillsSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private var skillsList: some View {
|
private var skillsList: some View {
|
||||||
VStack(spacing: 10) {
|
if self.model.skills.isEmpty {
|
||||||
ForEach(self.filteredSkills) { skill in
|
Text("No skills reported yet.")
|
||||||
SkillRow(
|
.foregroundStyle(.secondary)
|
||||||
skill: skill,
|
} else {
|
||||||
isBusy: self.model.isBusy(skill: skill),
|
List {
|
||||||
onToggleEnabled: { enabled in
|
ForEach(self.filteredSkills) { skill in
|
||||||
Task { await self.model.setEnabled(skillKey: skill.skillKey, enabled: enabled) }
|
SkillRow(
|
||||||
},
|
skill: skill,
|
||||||
onInstall: { option in
|
isBusy: self.model.isBusy(skill: skill),
|
||||||
Task { await self.model.install(skill: skill, option: option) }
|
onToggleEnabled: { enabled in
|
||||||
},
|
Task { await self.model.setEnabled(skillKey: skill.skillKey, enabled: enabled) }
|
||||||
onSetEnv: { envKey, isPrimary in
|
},
|
||||||
self.envEditor = EnvEditorState(
|
onInstall: { option in
|
||||||
skillKey: skill.skillKey,
|
Task { await self.model.install(skill: skill, option: option) }
|
||||||
skillName: skill.name,
|
},
|
||||||
envKey: envKey,
|
onSetEnv: { envKey, isPrimary in
|
||||||
isPrimary: isPrimary)
|
self.envEditor = EnvEditorState(
|
||||||
})
|
skillKey: skill.skillKey,
|
||||||
}
|
skillName: skill.name,
|
||||||
if !self.model.skills.isEmpty, self.filteredSkills.isEmpty {
|
envKey: envKey,
|
||||||
Text("No skills match this filter.")
|
isPrimary: isPrimary)
|
||||||
.font(.callout)
|
})
|
||||||
.foregroundStyle(.secondary)
|
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
}
|
||||||
.padding(.top, 4)
|
if !self.model.skills.isEmpty, self.filteredSkills.isEmpty {
|
||||||
|
Text("No skills match this filter.")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.listStyle(.inset)
|
||||||
|
.searchable(text: self.$searchQuery, placement: .automatic, prompt: "Search skills")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var filterBar: some View {
|
private var filterBar: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
TextField("Search skills", text: self.$searchQuery)
|
Text("Filter")
|
||||||
.textFieldStyle(.roundedBorder)
|
.font(.caption)
|
||||||
.frame(maxWidth: 320)
|
.foregroundStyle(.secondary)
|
||||||
Picker("Filter", selection: self.$filter) {
|
Picker("", selection: self.$filter) {
|
||||||
ForEach(SkillsFilter.allCases) { filter in
|
ForEach(SkillsFilter.allCases) { filter in
|
||||||
Text(filter.title)
|
Text(filter.title)
|
||||||
.tag(filter)
|
.tag(filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.labelsHidden()
|
||||||
.frame(maxWidth: 420)
|
.pickerStyle(.menu)
|
||||||
|
.frame(width: 160, alignment: .leading)
|
||||||
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,45 +180,44 @@ private struct SkillRow: View {
|
|||||||
private var missingConfig: [String] { self.skill.missing.config }
|
private var missingConfig: [String] { self.skill.missing.config }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
HStack(alignment: .top, spacing: 12) {
|
Text(self.skill.emoji ?? "✨")
|
||||||
Text(self.skill.emoji ?? "✨")
|
.font(.title2)
|
||||||
.font(.title2)
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||||
Text(self.skill.name)
|
Text(self.skill.name)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
self.statusBadge
|
self.statusBadge
|
||||||
}
|
|
||||||
Text(self.skill.description)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
self.metaRow
|
|
||||||
}
|
}
|
||||||
Spacer()
|
Text(self.skill.description)
|
||||||
}
|
.font(.subheadline)
|
||||||
|
|
||||||
if self.skill.disabled {
|
|
||||||
Text("Disabled in config")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
} else if !self.skill.eligible {
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
self.missingSummary
|
self.metaRow
|
||||||
|
|
||||||
|
if self.skill.disabled {
|
||||||
|
Text("Disabled in config")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else if !self.skill.eligible {
|
||||||
|
self.missingSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.skill.configChecks.isEmpty {
|
||||||
|
self.configChecksView
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.missingEnv.isEmpty {
|
||||||
|
self.envActionRow
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.skill.configChecks.isEmpty {
|
Spacer(minLength: 0)
|
||||||
self.configChecksView
|
|
||||||
}
|
|
||||||
|
|
||||||
self.actionRow
|
self.trailingActions
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(.vertical, 6)
|
||||||
.background(Color(nsColor: .controlBackgroundColor))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 10)
|
|
||||||
.stroke(Color.secondary.opacity(0.15), lineWidth: 1))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sourceLabel: String {
|
private var sourceLabel: String {
|
||||||
@@ -248,23 +261,10 @@ private struct SkillRow: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.link)
|
.buttonStyle(.link)
|
||||||
}
|
}
|
||||||
HStack(spacing: 6) {
|
|
||||||
Text(self.enabledLabel)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Toggle("", isOn: self.enabledBinding)
|
|
||||||
.toggleStyle(.switch)
|
|
||||||
.labelsHidden()
|
|
||||||
.disabled(self.isBusy)
|
|
||||||
}
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var enabledLabel: String {
|
|
||||||
self.skill.disabled ? "Disabled" : "Enabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var homepageUrl: URL? {
|
private var homepageUrl: URL? {
|
||||||
guard let raw = self.skill.homepage?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
guard let raw = self.skill.homepage?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
||||||
return nil
|
return nil
|
||||||
@@ -317,14 +317,8 @@ private struct SkillRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var actionRow: some View {
|
private var envActionRow: some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(self.installOptions) { option in
|
|
||||||
Button(option.label) { self.onInstall(option) }
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.disabled(self.isBusy)
|
|
||||||
}
|
|
||||||
|
|
||||||
ForEach(self.missingEnv, id: \.self) { envKey in
|
ForEach(self.missingEnv, id: \.self) { envKey in
|
||||||
let isPrimary = envKey == self.skill.primaryEnv
|
let isPrimary = envKey == self.skill.primaryEnv
|
||||||
Button(isPrimary ? "Set API Key" : "Set \(envKey)") {
|
Button(isPrimary ? "Set API Key" : "Set \(envKey)") {
|
||||||
@@ -333,11 +327,33 @@ private struct SkillRow: View {
|
|||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.disabled(self.isBusy)
|
.disabled(self.isBusy)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var trailingActions: some View {
|
||||||
|
VStack(alignment: .trailing, spacing: 8) {
|
||||||
|
if !self.installOptions.isEmpty {
|
||||||
|
ForEach(self.installOptions) { option in
|
||||||
|
Button(option.label) { self.onInstall(option) }
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.disabled(self.isBusy)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toggle("", isOn: self.enabledBinding)
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
.labelsHidden()
|
||||||
|
.disabled(self.isBusy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.isBusy {
|
||||||
|
ProgressView()
|
||||||
|
.controlSize(.small)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var installOptions: [SkillInstallOption] {
|
private var installOptions: [SkillInstallOption] {
|
||||||
guard !self.missingBins.isEmpty else { return [] }
|
guard !self.missingBins.isEmpty else { return [] }
|
||||||
let missing = Set(self.missingBins)
|
let missing = Set(self.missingBins)
|
||||||
|
|||||||
Reference in New Issue
Block a user