mirror of
https://github.com/tw93/Mole.git
synced 2026-02-12 09:58:31 +00:00
feat: Add VS Code launch/task configurations and refactor app icon and resource management.
This commit is contained in:
383
app/ContentView.swift
Normal file
383
app/ContentView.swift
Normal file
@@ -0,0 +1,383 @@
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user