feat(features): improve responsiveness and empty states across feature views

Use adaptive grid columns and atlasContentWidth for responsive layouts
in Overview, Apps, History, and SmartClean views. Add action buttons
to empty states. Use flexible height for Apps split view. Refine
SmartClean scan/plan UI with brand-tinted progress and status chips.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zhukang
2026-03-10 21:57:41 +08:00
parent 7da436a220
commit b7087c706e
4 changed files with 292 additions and 239 deletions

View File

@@ -130,7 +130,7 @@ public struct AppsFeatureView: View {
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
} }
.frame(height: 560) .frame(minHeight: 400, maxHeight: .infinity)
} }
} }
.onAppear(perform: syncSelection) .onAppear(perform: syncSelection)
@@ -186,7 +186,9 @@ public struct AppsFeatureView: View {
title: AtlasL10n.string("apps.list.empty.title"), title: AtlasL10n.string("apps.list.empty.title"),
detail: AtlasL10n.string("apps.list.empty.detail"), detail: AtlasL10n.string("apps.list.empty.detail"),
systemImage: "square.stack.3d.up.slash", systemImage: "square.stack.3d.up.slash",
tone: .neutral tone: .neutral,
actionTitle: AtlasL10n.string("emptystate.action.refresh"),
onAction: onRefreshApps
) )
} else { } else {
List(selection: $selectedAppID) { List(selection: $selectedAppID) {
@@ -351,6 +353,8 @@ private struct AppDetailView: View {
let onPreview: () -> Void let onPreview: () -> Void
let onUninstall: () -> Void let onUninstall: () -> Void
@State private var showUninstallConfirmation = false
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: AtlasSpacing.xl) { VStack(alignment: .leading, spacing: AtlasSpacing.xl) {
HStack(alignment: .top, spacing: AtlasSpacing.lg) { HStack(alignment: .top, spacing: AtlasSpacing.lg) {
@@ -442,7 +446,7 @@ private struct AppDetailView: View {
title: item.title, title: item.title,
subtitle: item.detail, subtitle: item.detail,
footnote: item.recoverable ? AtlasL10n.string("apps.preview.row.recoverable") : AtlasL10n.string("apps.preview.row.review"), footnote: item.recoverable ? AtlasL10n.string("apps.preview.row.recoverable") : AtlasL10n.string("apps.preview.row.review"),
systemImage: icon(for: item.kind), systemImage: item.kind.atlasSystemImage,
tone: item.recoverable ? .success : .warning tone: item.recoverable ? .success : .warning
) { ) {
AtlasStatusChip( AtlasStatusChip(
@@ -491,26 +495,26 @@ private struct AppDetailView: View {
private var uninstallButton: some View { private var uninstallButton: some View {
Button(isUninstalling ? AtlasL10n.string("apps.uninstall.running") : AtlasL10n.string("apps.uninstall.action")) { Button(isUninstalling ? AtlasL10n.string("apps.uninstall.running") : AtlasL10n.string("apps.uninstall.action")) {
onUninstall() showUninstallConfirmation = true
} }
.buttonStyle(.atlasPrimary) .buttonStyle(.atlasPrimary)
.disabled(isBusy || previewPlan == nil) .disabled(isBusy || previewPlan == nil)
.accessibilityIdentifier("apps.uninstall.\(app.id.uuidString)") .accessibilityIdentifier("apps.uninstall.\(app.id.uuidString)")
.accessibilityHint(AtlasL10n.string("apps.uninstall.hint")) .accessibilityHint(AtlasL10n.string("apps.uninstall.hint"))
} .confirmationDialog(
AtlasL10n.string("apps.confirm.uninstall.title"),
private func icon(for kind: ActionItem.Kind) -> String { isPresented: $showUninstallConfirmation,
switch kind { titleVisibility: .visible
case .removeCache: ) {
return "trash" Button(AtlasL10n.string("apps.uninstall.action"), role: .destructive) {
case .removeApp: onUninstall()
return "app.badge.minus" }
case .archiveFile: Button(AtlasL10n.string("confirm.cancel"), role: .cancel) {}
return "archivebox" } message: {
case .inspectPermission: Text(AtlasL10n.string("apps.confirm.uninstall.message", app.name))
return "lock.shield"
} }
} }
} }
private struct AppGroup: Identifiable { private struct AppGroup: Identifiable {

View File

@@ -119,7 +119,7 @@ public struct HistoryFeatureView: View {
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
} }
.frame(height: 560) .frame(minHeight: 400, maxHeight: .infinity)
} }
} }
} }
@@ -713,7 +713,7 @@ private struct HistoryTaskSidebarRow: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: AtlasSpacing.xs) { VStack(alignment: .leading, spacing: AtlasSpacing.xs) {
HStack(alignment: .center, spacing: AtlasSpacing.sm) { HStack(alignment: .center, spacing: AtlasSpacing.sm) {
Image(systemName: taskRun.kind.historySystemImage) Image(systemName: taskRun.kind.atlasSystemImage)
.font(AtlasTypography.caption) .font(AtlasTypography.caption)
.foregroundStyle(taskRun.status.atlasTone.tint) .foregroundStyle(taskRun.status.atlasTone.tint)
.accessibilityHidden(true) .accessibilityHidden(true)
@@ -1049,37 +1049,7 @@ private enum HistoryRecoveryCategory: String, CaseIterable {
} }
} }
private extension TaskKind {
var historySystemImage: String {
switch self {
case .scan:
return "sparkles"
case .executePlan:
return "play.circle"
case .uninstallApp:
return "trash"
case .restore:
return "arrow.uturn.backward.circle"
case .inspectPermissions:
return "lock.shield"
}
}
}
private extension TaskStatus { private extension TaskStatus {
var atlasTone: AtlasTone {
switch self {
case .queued:
return .neutral
case .running:
return .warning
case .completed:
return .success
case .failed, .cancelled:
return .danger
}
}
var historyCalloutTitle: String { var historyCalloutTitle: String {
switch self { switch self {
case .queued: case .queued:

View File

@@ -6,13 +6,28 @@ import SwiftUI
public struct OverviewFeatureView: View { public struct OverviewFeatureView: View {
private let snapshot: AtlasWorkspaceSnapshot private let snapshot: AtlasWorkspaceSnapshot
private let isRefreshingHealthSnapshot: Bool private let isRefreshingHealthSnapshot: Bool
private let onStartSmartClean: (() -> Void)?
private let onNavigateToSmartClean: (() -> Void)?
private let onNavigateToHistory: (() -> Void)?
private let onNavigateToPermissions: (() -> Void)?
@Environment(\.atlasContentWidth) private var contentWidth
@State private var showAllOptimizations = false
public init( public init(
snapshot: AtlasWorkspaceSnapshot = AtlasScaffoldWorkspace.snapshot(), snapshot: AtlasWorkspaceSnapshot = AtlasScaffoldWorkspace.snapshot(),
isRefreshingHealthSnapshot: Bool = false isRefreshingHealthSnapshot: Bool = false,
onStartSmartClean: (() -> Void)? = nil,
onNavigateToSmartClean: (() -> Void)? = nil,
onNavigateToHistory: (() -> Void)? = nil,
onNavigateToPermissions: (() -> Void)? = nil
) { ) {
self.snapshot = snapshot self.snapshot = snapshot
self.isRefreshingHealthSnapshot = isRefreshingHealthSnapshot self.isRefreshingHealthSnapshot = isRefreshingHealthSnapshot
self.onStartSmartClean = onStartSmartClean
self.onNavigateToSmartClean = onNavigateToSmartClean
self.onNavigateToHistory = onNavigateToHistory
self.onNavigateToPermissions = onNavigateToPermissions
} }
public var body: some View { public var body: some View {
@@ -27,15 +42,18 @@ public struct OverviewFeatureView: View {
systemImage: overviewCalloutTone.symbol systemImage: overviewCalloutTone.symbol
) )
LazyVGrid(columns: AtlasLayout.metricColumns, spacing: AtlasSpacing.lg) { // MARK: - Hero metric full width
AtlasMetricCard( AtlasMetricCard(
title: AtlasL10n.string("overview.metric.reclaimable.title"), title: AtlasL10n.string("overview.metric.reclaimable.title"),
value: AtlasFormatters.byteCount(snapshot.reclaimableSpaceBytes), value: AtlasFormatters.byteCount(snapshot.reclaimableSpaceBytes),
detail: AtlasL10n.string("overview.metric.reclaimable.detail"), detail: AtlasL10n.string("overview.metric.reclaimable.detail"),
tone: .success, tone: .success,
systemImage: "sparkles", systemImage: "sparkles",
elevation: .prominent elevation: .prominent
) )
// MARK: - Secondary metrics adaptive 2/1 columns
LazyVGrid(columns: secondaryColumns, spacing: AtlasSpacing.lg) {
AtlasMetricCard( AtlasMetricCard(
title: AtlasL10n.string("overview.metric.findings.title"), title: AtlasL10n.string("overview.metric.findings.title"),
value: "\(snapshot.findings.count)", value: "\(snapshot.findings.count)",
@@ -54,72 +72,13 @@ public struct OverviewFeatureView: View {
) )
} }
AtlasInfoCard( // MARK: - Quick actions bar
title: AtlasL10n.string("overview.snapshot.title"), quickActionsBar
subtitle: AtlasL10n.string("overview.snapshot.subtitle")
) {
if isRefreshingHealthSnapshot, snapshot.healthSnapshot == nil {
AtlasLoadingState(
title: AtlasL10n.string("overview.snapshot.loading.title"),
detail: AtlasL10n.string("overview.snapshot.loading.detail")
)
} else if let healthSnapshot = snapshot.healthSnapshot {
LazyVGrid(columns: AtlasLayout.metricColumns, spacing: AtlasSpacing.lg) {
AtlasMetricCard(
title: AtlasL10n.string("overview.snapshot.memory.title"),
value: "\(formatted(healthSnapshot.memoryUsedGB))/\(formatted(healthSnapshot.memoryTotalGB)) GB",
detail: AtlasL10n.string("overview.snapshot.memory.detail"),
tone: healthSnapshot.memoryUsedGB / max(healthSnapshot.memoryTotalGB, 1) > 0.75 ? .warning : .neutral,
systemImage: "memorychip"
)
AtlasMetricCard(
title: AtlasL10n.string("overview.snapshot.disk.title"),
value: "\(formatted(healthSnapshot.diskUsedPercent))%",
detail: AtlasL10n.string("overview.snapshot.disk.detail", formatted(healthSnapshot.diskUsedGB), formatted(healthSnapshot.diskTotalGB)),
tone: healthSnapshot.diskUsedPercent > 80 ? .warning : .success,
systemImage: "internaldrive"
)
AtlasMetricCard(
title: AtlasL10n.string("overview.snapshot.uptime.title"),
value: "\(formatted(healthSnapshot.uptimeDays)) \(AtlasL10n.string("common.days"))",
detail: AtlasL10n.string("overview.snapshot.uptime.detail"),
tone: .neutral,
systemImage: "clock"
)
}
AtlasCallout( // MARK: - System Snapshot (flattened no InfoCard wrapper)
title: healthSnapshot.diskUsedPercent > 80 ? AtlasL10n.string("overview.snapshot.callout.warning.title") : AtlasL10n.string("overview.snapshot.callout.ok.title"), systemSnapshotSection
detail: healthSnapshot.diskUsedPercent > 80
? AtlasL10n.string("overview.snapshot.callout.warning.detail")
: AtlasL10n.string("overview.snapshot.callout.ok.detail"),
tone: healthSnapshot.diskUsedPercent > 80 ? .warning : .success,
systemImage: healthSnapshot.diskUsedPercent > 80 ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
)
VStack(alignment: .leading, spacing: AtlasSpacing.md) {
ForEach(Array(healthSnapshot.optimizations.prefix(4))) { optimization in
AtlasDetailRow(
title: optimization.name,
subtitle: optimization.detail,
footnote: AtlasL10n.localizedCategory(optimization.category).capitalized,
systemImage: optimization.isSafe ? "checkmark.shield" : "slider.horizontal.3",
tone: optimization.isSafe ? .success : .warning
) {
AtlasStatusChip(optimization.isSafe ? AtlasL10n.string("risk.safe") : AtlasL10n.string("risk.review"), tone: optimization.isSafe ? .success : .warning)
}
}
}
} else {
AtlasEmptyState(
title: AtlasL10n.string("overview.snapshot.empty.title"),
detail: AtlasL10n.string("overview.snapshot.empty.detail"),
systemImage: "waveform.path.ecg",
tone: .warning
)
}
}
// MARK: - Recommended Actions
AtlasInfoCard( AtlasInfoCard(
title: AtlasL10n.string("overview.actions.title"), title: AtlasL10n.string("overview.actions.title"),
subtitle: AtlasL10n.string("overview.actions.subtitle") subtitle: AtlasL10n.string("overview.actions.subtitle")
@@ -138,7 +97,7 @@ public struct OverviewFeatureView: View {
title: finding.title, title: finding.title,
subtitle: finding.detail, subtitle: finding.detail,
footnote: "\(AtlasL10n.localizedCategory(finding.category))\(riskSupport(for: finding.risk))", footnote: "\(AtlasL10n.localizedCategory(finding.category))\(riskSupport(for: finding.risk))",
systemImage: icon(for: finding.category), systemImage: AtlasCategoryIcon.systemImage(for: finding.category),
tone: finding.risk.atlasTone tone: finding.risk.atlasTone
) { ) {
VStack(alignment: .trailing, spacing: AtlasSpacing.sm) { VStack(alignment: .trailing, spacing: AtlasSpacing.sm) {
@@ -149,10 +108,24 @@ public struct OverviewFeatureView: View {
} }
} }
} }
if snapshot.findings.count > 4, let onNavigateToSmartClean {
Button {
onNavigateToSmartClean()
} label: {
Label(
AtlasL10n.string("overview.actions.viewAll", snapshot.findings.count),
systemImage: "arrow.right.circle"
)
}
.buttonStyle(.atlasGhost)
.frame(maxWidth: .infinity, alignment: .center)
}
} }
} }
} }
// MARK: - Recent Activity
AtlasInfoCard( AtlasInfoCard(
title: AtlasL10n.string("overview.activity.title"), title: AtlasL10n.string("overview.activity.title"),
subtitle: AtlasL10n.string("overview.activity.subtitle") subtitle: AtlasL10n.string("overview.activity.subtitle")
@@ -171,18 +144,193 @@ public struct OverviewFeatureView: View {
title: taskRun.kind.title, title: taskRun.kind.title,
subtitle: taskRun.summary, subtitle: taskRun.summary,
footnote: timelineFootnote(for: taskRun), footnote: timelineFootnote(for: taskRun),
systemImage: icon(for: taskRun.kind), systemImage: taskRun.kind.atlasSystemImage,
tone: taskRun.status.atlasTone tone: taskRun.status.atlasTone
) { ) {
AtlasStatusChip(taskRun.status.title, tone: taskRun.status.atlasTone) AtlasStatusChip(taskRun.status.title, tone: taskRun.status.atlasTone)
} }
} }
if snapshot.taskRuns.count > 3, let onNavigateToHistory {
Button {
onNavigateToHistory()
} label: {
Label(
AtlasL10n.string("overview.activity.viewAll"),
systemImage: "arrow.right.circle"
)
}
.buttonStyle(.atlasGhost)
.frame(maxWidth: .infinity, alignment: .center)
}
} }
} }
} }
} }
} }
// MARK: - Quick Actions Bar
@ViewBuilder
private var quickActionsBar: some View {
ViewThatFits(in: .horizontal) {
HStack(spacing: AtlasSpacing.lg) {
quickActionButtons
}
VStack(spacing: AtlasSpacing.md) {
quickActionButtons
}
}
}
@ViewBuilder
private var quickActionButtons: some View {
if let onStartSmartClean {
Button {
onStartSmartClean()
} label: {
Label(
AtlasL10n.string("overview.action.smartClean"),
systemImage: AtlasIcon.smartClean
)
}
.buttonStyle(.atlasPrimary)
.accessibilityHint(AtlasL10n.string("overview.action.smartClean.hint"))
}
if !requiredPermissionsReady, let onNavigateToPermissions {
Button {
onNavigateToPermissions()
} label: {
Label(
AtlasL10n.string("overview.action.permissions"),
systemImage: AtlasIcon.permissions
)
}
.buttonStyle(.atlasSecondary)
.accessibilityHint(AtlasL10n.string("overview.action.permissions.hint"))
}
}
// MARK: - System Snapshot Section (flattened)
@ViewBuilder
private var systemSnapshotSection: some View {
VStack(alignment: .leading, spacing: AtlasSpacing.xl) {
// Section header
VStack(alignment: .leading, spacing: AtlasSpacing.xs) {
Text(AtlasL10n.string("overview.snapshot.title"))
.font(AtlasTypography.sectionTitle)
Text(AtlasL10n.string("overview.snapshot.subtitle"))
.font(AtlasTypography.body)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
if isRefreshingHealthSnapshot, snapshot.healthSnapshot == nil {
AtlasLoadingState(
title: AtlasL10n.string("overview.snapshot.loading.title"),
detail: AtlasL10n.string("overview.snapshot.loading.detail")
)
} else if let healthSnapshot = snapshot.healthSnapshot {
LazyVGrid(columns: adaptiveColumns, spacing: AtlasSpacing.lg) {
AtlasMetricCard(
title: AtlasL10n.string("overview.snapshot.memory.title"),
value: "\(formatted(healthSnapshot.memoryUsedGB))/\(formatted(healthSnapshot.memoryTotalGB)) GB",
detail: AtlasL10n.string("overview.snapshot.memory.detail"),
tone: healthSnapshot.memoryUsedGB / max(healthSnapshot.memoryTotalGB, 1) > 0.75 ? .warning : .neutral,
systemImage: "memorychip"
)
AtlasMetricCard(
title: AtlasL10n.string("overview.snapshot.disk.title"),
value: "\(formatted(healthSnapshot.diskUsedPercent))%",
detail: AtlasL10n.string("overview.snapshot.disk.detail", formatted(healthSnapshot.diskUsedGB), formatted(healthSnapshot.diskTotalGB)),
tone: healthSnapshot.diskUsedPercent > 80 ? .warning : .success,
systemImage: "internaldrive"
)
AtlasMetricCard(
title: AtlasL10n.string("overview.snapshot.uptime.title"),
value: "\(formatted(healthSnapshot.uptimeDays)) \(AtlasL10n.string("common.days"))",
detail: AtlasL10n.string("overview.snapshot.uptime.detail"),
tone: .neutral,
systemImage: "clock"
)
}
AtlasCallout(
title: healthSnapshot.diskUsedPercent > 80 ? AtlasL10n.string("overview.snapshot.callout.warning.title") : AtlasL10n.string("overview.snapshot.callout.ok.title"),
detail: healthSnapshot.diskUsedPercent > 80
? AtlasL10n.string("overview.snapshot.callout.warning.detail")
: AtlasL10n.string("overview.snapshot.callout.ok.detail"),
tone: healthSnapshot.diskUsedPercent > 80 ? .warning : .success,
systemImage: healthSnapshot.diskUsedPercent > 80 ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
)
VStack(alignment: .leading, spacing: AtlasSpacing.md) {
let optimizations = healthSnapshot.optimizations
let visibleOptimizations = showAllOptimizations ? optimizations : Array(optimizations.prefix(4))
ForEach(Array(visibleOptimizations)) { optimization in
AtlasDetailRow(
title: optimization.name,
subtitle: optimization.detail,
footnote: AtlasL10n.localizedCategory(optimization.category).capitalized,
systemImage: optimization.isSafe ? "checkmark.shield" : "slider.horizontal.3",
tone: optimization.isSafe ? .success : .warning
) {
AtlasStatusChip(optimization.isSafe ? AtlasL10n.string("risk.safe") : AtlasL10n.string("risk.review"), tone: optimization.isSafe ? .success : .warning)
}
}
if optimizations.count > 4 {
Button {
withAnimation(AtlasMotion.standard) {
showAllOptimizations.toggle()
}
} label: {
Label(
showAllOptimizations
? AtlasL10n.string("overview.snapshot.collapseOptimizations")
: AtlasL10n.string("overview.snapshot.viewAllOptimizations", optimizations.count),
systemImage: showAllOptimizations ? "chevron.up" : "chevron.down"
)
}
.buttonStyle(.atlasGhost)
.frame(maxWidth: .infinity, alignment: .center)
}
}
} else {
AtlasEmptyState(
title: AtlasL10n.string("overview.snapshot.empty.title"),
detail: AtlasL10n.string("overview.snapshot.empty.detail"),
systemImage: "waveform.path.ecg",
tone: .warning
)
}
}
}
// MARK: - Adaptive Columns
private var adaptiveColumns: [GridItem] {
AtlasLayout.adaptiveMetricColumns(for: contentWidth)
}
private var secondaryColumns: [GridItem] {
contentWidth >= 420
? [
GridItem(.flexible(minimum: 180), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 180), spacing: AtlasSpacing.lg),
]
: [
GridItem(.flexible(minimum: 180), spacing: AtlasSpacing.lg),
]
}
// MARK: - Computed Properties
private var requiredPermissionStates: [PermissionState] { private var requiredPermissionStates: [PermissionState] {
snapshot.permissions.filter { $0.kind.isRequiredForCurrentWorkflows } snapshot.permissions.filter { $0.kind.isRequiredForCurrentWorkflows }
} }
@@ -238,61 +386,4 @@ public struct OverviewFeatureView: View {
return AtlasL10n.string("overview.activity.timeline.running", start) return AtlasL10n.string("overview.activity.timeline.running", start)
} }
private func icon(for category: String) -> String {
switch category.lowercased() {
case "developer":
return "hammer"
case "system":
return "gearshape.2"
case "apps":
return "square.stack.3d.up"
case "browsers":
return "globe"
default:
return "sparkles"
}
}
private func icon(for kind: TaskKind) -> String {
switch kind {
case .scan:
return "sparkles"
case .executePlan:
return "play.circle"
case .uninstallApp:
return "trash"
case .restore:
return "arrow.uturn.backward.circle"
case .inspectPermissions:
return "lock.shield"
}
}
}
private extension RiskLevel {
var atlasTone: AtlasTone {
switch self {
case .safe:
return .success
case .review:
return .warning
case .advanced:
return .danger
}
}
}
private extension TaskStatus {
var atlasTone: AtlasTone {
switch self {
case .queued:
return .neutral
case .running:
return .warning
case .completed:
return .success
case .failed, .cancelled:
return .danger
}
}
} }

View File

@@ -16,6 +16,8 @@ public struct SmartCleanFeatureView: View {
private let onRefreshPreview: () -> Void private let onRefreshPreview: () -> Void
private let onExecutePlan: () -> Void private let onExecutePlan: () -> Void
@State private var showExecuteConfirmation = false
public init( public init(
findings: [Finding] = AtlasScaffoldFixtures.findings, findings: [Finding] = AtlasScaffoldFixtures.findings,
plan: ActionPlan = AtlasScaffoldFixtures.actionPlan, plan: ActionPlan = AtlasScaffoldFixtures.actionPlan,
@@ -77,14 +79,13 @@ public struct SmartCleanFeatureView: View {
if scanProgress > 0 { if scanProgress > 0 {
ProgressView(value: max(scanProgress, 0), total: 1) ProgressView(value: max(scanProgress, 0), total: 1)
.controlSize(.large) .controlSize(.large)
.tint(AtlasColor.brand)
} }
AtlasCallout( Text(primaryAction.detail)
title: primaryAction.title, .font(AtlasTypography.body)
detail: primaryAction.detail, .foregroundStyle(.secondary)
tone: primaryAction.tone, .fixedSize(horizontal: false, vertical: true)
systemImage: primaryAction.systemImage
)
ViewThatFits(in: .horizontal) { ViewThatFits(in: .horizontal) {
HStack(alignment: .center, spacing: AtlasSpacing.md) { HStack(alignment: .center, spacing: AtlasSpacing.md) {
@@ -159,21 +160,25 @@ public struct SmartCleanFeatureView: View {
) )
} }
AtlasCallout( if plan.items.isEmpty || manualReviewCount > 0 {
title: manualReviewCount == 0 ? AtlasL10n.string("smartclean.preview.callout.safe.title") : AtlasL10n.string("smartclean.preview.callout.review.title"), AtlasCallout(
detail: manualReviewCount == 0 title: manualReviewCount == 0 ? AtlasL10n.string("smartclean.preview.callout.safe.title") : AtlasL10n.string("smartclean.preview.callout.review.title"),
? AtlasL10n.string("smartclean.preview.callout.safe.detail") detail: manualReviewCount == 0
: AtlasL10n.string("smartclean.preview.callout.review.detail"), ? AtlasL10n.string("smartclean.preview.callout.safe.detail")
tone: manualReviewCount == 0 ? .success : .warning, : AtlasL10n.string("smartclean.preview.callout.review.detail"),
systemImage: manualReviewCount == 0 ? "checkmark.shield.fill" : "exclamationmark.triangle.fill" tone: manualReviewCount == 0 ? .success : .warning,
) systemImage: manualReviewCount == 0 ? "checkmark.shield.fill" : "exclamationmark.triangle.fill"
)
}
if plan.items.isEmpty { if plan.items.isEmpty {
AtlasEmptyState( AtlasEmptyState(
title: AtlasL10n.string("smartclean.preview.empty.title"), title: AtlasL10n.string("smartclean.preview.empty.title"),
detail: AtlasL10n.string("smartclean.preview.empty.detail"), detail: AtlasL10n.string("smartclean.preview.empty.detail"),
systemImage: "list.bullet.clipboard", systemImage: "list.bullet.clipboard",
tone: .neutral tone: .neutral,
actionTitle: AtlasL10n.string("emptystate.action.startScan"),
onAction: onStartScan
) )
} else { } else {
VStack(alignment: .leading, spacing: AtlasSpacing.md) { VStack(alignment: .leading, spacing: AtlasSpacing.md) {
@@ -182,7 +187,7 @@ public struct SmartCleanFeatureView: View {
title: item.title, title: item.title,
subtitle: item.detail, subtitle: item.detail,
footnote: supportText(for: item.kind), footnote: supportText(for: item.kind),
systemImage: icon(for: item.kind), systemImage: item.kind.atlasSystemImage,
tone: item.recoverable ? .success : .warning tone: item.recoverable ? .success : .warning
) { ) {
VStack(alignment: .trailing, spacing: AtlasSpacing.xs) { VStack(alignment: .trailing, spacing: AtlasSpacing.xs) {
@@ -205,7 +210,9 @@ public struct SmartCleanFeatureView: View {
title: AtlasL10n.string("smartclean.empty.title"), title: AtlasL10n.string("smartclean.empty.title"),
detail: AtlasL10n.string("smartclean.empty.detail"), detail: AtlasL10n.string("smartclean.empty.detail"),
systemImage: "sparkles.tv", systemImage: "sparkles.tv",
tone: .neutral tone: .neutral,
actionTitle: AtlasL10n.string("emptystate.action.startScan"),
onAction: onStartScan
) )
} else { } else {
ForEach(RiskLevel.allCases, id: \.self) { risk in ForEach(RiskLevel.allCases, id: \.self) { risk in
@@ -231,7 +238,7 @@ public struct SmartCleanFeatureView: View {
title: finding.title, title: finding.title,
subtitle: finding.detail, subtitle: finding.detail,
footnote: "\(AtlasL10n.localizedCategory(finding.category))\(actionExpectation(for: finding.risk))", footnote: "\(AtlasL10n.localizedCategory(finding.category))\(actionExpectation(for: finding.risk))",
systemImage: icon(for: finding.category), systemImage: AtlasCategoryIcon.systemImage(for: finding.category),
tone: risk.atlasTone tone: risk.atlasTone
) { ) {
VStack(alignment: .trailing, spacing: AtlasSpacing.sm) { VStack(alignment: .trailing, spacing: AtlasSpacing.sm) {
@@ -381,8 +388,16 @@ public struct SmartCleanFeatureView: View {
return .refresh return .refresh
} }
private func primaryActionTapped() {
if primaryAction == .execute {
showExecuteConfirmation = true
} else {
primaryAction.handler(startScan: onStartScan, refreshPreview: onRefreshPreview, executePlan: onExecutePlan)()
}
}
private var primaryActionButton: some View { private var primaryActionButton: some View {
Button(action: primaryAction.handler(startScan: onStartScan, refreshPreview: onRefreshPreview, executePlan: onExecutePlan)) { Button(action: primaryActionTapped) {
Label(primaryAction.buttonTitle, systemImage: primaryAction.buttonSystemImage) Label(primaryAction.buttonTitle, systemImage: primaryAction.buttonSystemImage)
} }
.buttonStyle(.atlasPrimary) .buttonStyle(.atlasPrimary)
@@ -390,6 +405,18 @@ public struct SmartCleanFeatureView: View {
.disabled(primaryAction.isDisabled(canExecutePlan: canExecutePlan)) .disabled(primaryAction.isDisabled(canExecutePlan: canExecutePlan))
.accessibilityIdentifier(primaryAction.accessibilityIdentifier) .accessibilityIdentifier(primaryAction.accessibilityIdentifier)
.accessibilityHint(primaryAction.accessibilityHint) .accessibilityHint(primaryAction.accessibilityHint)
.confirmationDialog(
AtlasL10n.string("smartclean.confirm.execute.title"),
isPresented: $showExecuteConfirmation,
titleVisibility: .visible
) {
Button(AtlasL10n.string("smartclean.action.execute"), role: .destructive) {
onExecutePlan()
}
Button(AtlasL10n.string("confirm.cancel"), role: .cancel) {}
} message: {
Text(AtlasL10n.string("smartclean.confirm.execute.message"))
}
} }
@ViewBuilder @ViewBuilder
@@ -451,33 +478,6 @@ public struct SmartCleanFeatureView: View {
} }
} }
private func icon(for kind: ActionItem.Kind) -> String {
switch kind {
case .removeCache:
return "trash"
case .removeApp:
return "app.badge.minus"
case .archiveFile:
return "archivebox"
case .inspectPermission:
return "lock.shield"
}
}
private func icon(for category: String) -> String {
switch category.lowercased() {
case "developer":
return "hammer"
case "system":
return "gearshape.2"
case "apps":
return "square.stack.3d.up"
case "browsers":
return "globe"
default:
return "sparkles"
}
}
} }
private enum SmartCleanPrimaryAction: Equatable { private enum SmartCleanPrimaryAction: Equatable {
@@ -596,15 +596,3 @@ private enum SmartCleanPrimaryAction: Equatable {
} }
} }
private extension RiskLevel {
var atlasTone: AtlasTone {
switch self {
case .safe:
return .success
case .review:
return .warning
case .advanced:
return .danger
}
}
}