mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 19:09:43 +00:00
384 lines
11 KiB
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() }
|
|
}
|
|
}
|
|
}
|