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:
@@ -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 {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user