1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 18:34:46 +00:00
Files
Mole/app/ContentView.swift

384 lines
11 KiB
Swift

import SwiftUI
struct ContentView: View {
@State private var appState: AppState = .idle
@State private var appMode: AppMode = .cleaner // New Mode State
@State private var logs: [String] = []
@State private var showAppList = false
@State private var showCelebration = false
@State private var celebrationColors: [Color] = []
@State private var celebrationMessage: String = ""
@Namespace private var animationNamespace
// Connect to Real Logic
@StateObject private var scanner = ScannerService()
@StateObject private var uninstaller = UninstallerService()
@StateObject private var optimizer = OptimizerService()
@ObservedObject var authContext = AuthContext.shared
// Mercury (Cleaner) - Dark Industrial Gray
let mercuryColor = Color(red: 0.15, green: 0.15, blue: 0.18)
// Mars (Uninstaller) - Deep Red
let marsColor = Color(red: 0.25, green: 0.08, blue: 0.05)
// Earth (Optimizer) - Deep Blue
let earthColor = Color(red: 0.05, green: 0.1, blue: 0.25)
var body: some View {
ZStack {
// Dynamic Background
Color.black.ignoresSafeArea()
RadialGradient(
gradient: Gradient(colors: [
appMode == .cleaner ? mercuryColor : (appMode == .uninstaller ? marsColor : earthColor),
.black,
]),
center: .center,
startRadius: 0,
endRadius: 600
)
.ignoresSafeArea()
.animation(.easeInOut(duration: 0.5), value: appMode)
// Custom Top Tab Bar
VStack {
TopBarView(
appMode: $appMode, animationNamespace: animationNamespace, authContext: authContext
)
.padding(.top, 20)
Spacer()
}
VStack(spacing: -10) {
Spacer()
// The Mole (Interactive) & Draggable
// The Mole (Interactive) & Draggable
MoleView(
state: $appState,
appMode: $appMode,
isRunning: scanner.isScanning || scanner.isCleaning || optimizer.isOptimizing
|| uninstaller.isUninstalling
)
.gesture(
DragGesture()
.onEnded { value in
if value.translation.width < -50 {
withAnimation { appMode = .uninstaller }
} else if value.translation.width > 50 {
withAnimation { appMode = .cleaner }
}
}
)
.onHover { inside in
if inside {
NSCursor.pointingHand.set()
} else {
NSCursor.arrow.set()
}
}
.opacity(showAppList ? 0.0 : 1.0) // Hide when list is open
.animation(.easeInOut, value: showAppList)
.padding(.top, 75) // Visual centering adjustment
Spacer() // Dynamic spacing
// Status Area
ZStack {
// Logs overlay
if case .scanning = appState, appMode == .cleaner {
LogView(logs: logs)
.transition(.opacity)
} else if case .cleaning = appState, appMode == .cleaner {
LogView(logs: logs)
.transition(.opacity)
} else if appMode == .optimizer && optimizer.isOptimizing {
LogView(logs: logs)
.transition(.opacity)
} else if appMode == .uninstaller && uninstaller.isUninstalling {
LogView(logs: logs)
.transition(.opacity)
} else if showAppList {
// Showing App List? No status text needed or handled by overlay
EmptyView()
} else {
// Action Button
Button(action: {
if appMode == .cleaner {
startSmartClean()
} else if appMode == .uninstaller {
handleUninstallerAction()
} else {
handleOptimizerAction()
}
}) {
HStack(spacing: 8) {
if scanner.isScanning || scanner.isCleaning || optimizer.isOptimizing
|| uninstaller.isUninstalling
{
ProgressView()
.controlSize(.small)
.tint(.black)
}
Text("Mole")
.font(.system(size: 14, weight: .bold, design: .monospaced))
}
.frame(minWidth: 140)
.padding(.vertical, 12)
.background(Color.white)
.foregroundStyle(.black)
.clipShape(Capsule())
.shadow(color: .white.opacity(0.2), radius: 10, x: 0, y: 0)
}
.buttonStyle(.plain)
.disabled(
scanner.isScanning || scanner.isCleaning || optimizer.isOptimizing
|| uninstaller.isUninstalling
)
.onHover { inside in
if inside { NSCursor.pointingHand.set() } else { NSCursor.arrow.set() }
}
}
}
.frame(height: 100)
.padding(.bottom, 30) // Anchor to bottom
}
// App List Overlay
if showAppList {
AppListView(
apps: uninstaller.apps,
onSelect: { app in
handleUninstall(app)
},
onDismiss: {
withAnimation { showAppList = false }
}
)
.frame(width: 400, height: 550)
.transition(.move(edge: .bottom).combined(with: .opacity))
.zIndex(10)
}
if showCelebration {
VStack(spacing: 8) {
ConfettiView(colors: celebrationColors)
.offset(y: -50)
VStack(spacing: 4) {
Text("Success!")
.font(.system(size: 24, weight: .bold, design: .rounded))
.foregroundStyle(.white)
.shadow(radius: 5)
if !celebrationMessage.isEmpty {
Text(celebrationMessage)
.font(.system(size: 14, weight: .medium, design: .monospaced))
.foregroundStyle(.white.opacity(0.9))
.shadow(radius: 5)
}
}
.offset(y: 105)
}
.allowsHitTesting(false)
.zIndex(100)
.transition(.scale.combined(with: .opacity))
}
}
.frame(minWidth: 600, minHeight: 500)
.onChange(of: scanner.currentLog) {
if !scanner.currentLog.isEmpty {
withAnimation(.spring) {
if appMode == .cleaner {
logs.append(scanner.currentLog)
}
}
}
}
.onChange(of: optimizer.currentLog) {
if !optimizer.currentLog.isEmpty {
withAnimation(.spring) {
if appMode == .optimizer {
logs.append(optimizer.currentLog)
}
}
}
}
.onChange(of: uninstaller.currentLog) {
if !uninstaller.currentLog.isEmpty {
withAnimation(.spring) {
if appMode == .uninstaller {
logs.append(uninstaller.currentLog)
}
}
}
}
.sheet(isPresented: $authContext.needsPassword) {
PasswordSheetView(onUnlock: {
// Unlock success implies AuthContext.password is set.
// Services will use it on next attempt.
})
}
.onChange(of: appMode) {
appState = .idle
logs.removeAll()
showAppList = false
showCelebration = false
scanner.reset()
optimizer.reset()
uninstaller.reset()
}
}
// MARK: - Actions
func startSmartClean() {
withAnimation {
appState = .scanning
logs.removeAll()
showCelebration = false // Dismiss old success
}
Task {
await scanner.startScan()
try? await Task.sleep(nanoseconds: 500_000_000)
await MainActor.run {
if scanner.totalSize > 0 {
startCleaning()
} else {
withAnimation {
appState = .idle
logs.removeAll()
}
triggerCelebration([.white], message: "Already Clean")
}
}
}
}
func startCleaning() {
withAnimation {
appState = .cleaning
logs.removeAll()
showCelebration = false
}
Task {
let cleanedBytes = await scanner.cleanSystem()
withAnimation {
appState = .done
}
let mb = Double(cleanedBytes) / 1024.0 / 1024.0
let msg =
mb > 1024
? String(format: "Cleaned %.1f GB", mb / 1024.0) : String(format: "Cleaned %.0f MB", mb)
triggerCelebration([.orange, .red, .yellow, .white], message: msg)
}
}
func handleUninstallerAction() {
withAnimation {
showAppList = true
showCelebration = false
}
Task { await uninstaller.scanApps() }
}
func handleUninstall(_ app: AppItem) {
withAnimation {
showAppList = false
logs.removeAll()
showCelebration = false
}
Task {
await uninstaller.uninstall(app)
triggerCelebration(
[.red, .orange, .yellow, .green, .blue, .purple, .pink, .mint],
message: "Uninstalled \(app.name)")
}
}
func handleOptimizerAction() {
showCelebration = false // Immediate dismiss
Task {
await optimizer.optimize()
triggerCelebration([.cyan, .blue, .purple, .mint, .white], message: "Optimized")
}
}
func triggerCelebration(_ colors: [Color], message: String = "") {
celebrationColors = colors
celebrationMessage = message
withAnimation(.spring(response: 0.5, dampingFraction: 0.6)) { showCelebration = true }
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
withAnimation { showCelebration = false }
}
}
}
struct TopBarView: View {
@Binding var appMode: AppMode
var animationNamespace: Namespace.ID
@ObservedObject var authContext: AuthContext
var body: some View {
ZStack {
HStack(spacing: 0) {
TabBarButton(mode: .cleaner, appMode: $appMode, namespace: animationNamespace)
TabBarButton(mode: .uninstaller, appMode: $appMode, namespace: animationNamespace)
TabBarButton(mode: .optimizer, appMode: $appMode, namespace: animationNamespace)
}
.padding(4)
.background(Capsule().fill(.ultraThinMaterial).opacity(0.3))
}
}
}
struct TabBarButton: View {
let mode: AppMode
@Binding var appMode: AppMode
var namespace: Namespace.ID
var title: String {
switch mode {
case .cleaner: return "clean"
case .uninstaller: return "uninstall"
case .optimizer: return "optimize"
}
}
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
appMode = mode
}
}) {
Text(title)
.font(.system(size: 12, weight: .bold, design: .monospaced))
.foregroundStyle(appMode == mode ? .black : .white.opacity(0.6))
.padding(.vertical, 8)
.padding(.horizontal, 16)
.background(
ZStack {
if appMode == mode {
Capsule()
.fill(Color.white)
.matchedGeometryEffect(id: "TabHighlight", in: namespace)
}
}
)
}
.buttonStyle(.plain)
.onHover { inside in
if inside { NSCursor.pointingHand.set() } else { NSCursor.arrow.set() }
}
}
}