mirror of
https://github.com/tw93/Mole.git
synced 2026-02-16 08:21:18 +00:00
feat: introduce multi-mode functionality for Cleaner, Uninstaller, and Optimizer with dynamic UI and dedicated services.
This commit is contained in:
@@ -11,7 +11,10 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "Mole"
|
name: "Mole",
|
||||||
|
resources: [
|
||||||
|
.process("Resources")
|
||||||
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
77
app/Mole/Sources/Mole/AppListView.swift
Normal file
77
app/Mole/Sources/Mole/AppListView.swift
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AppListView: View {
|
||||||
|
let apps: [AppItem]
|
||||||
|
var onSelect: (AppItem) -> Void
|
||||||
|
var onDismiss: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
// Header
|
||||||
|
HStack {
|
||||||
|
Text("Installed Apps")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
Spacer()
|
||||||
|
Button(action: onDismiss) {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.foregroundStyle(.gray)
|
||||||
|
.font(.title2)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.black.opacity(0.8))
|
||||||
|
|
||||||
|
// List
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(spacing: 12) {
|
||||||
|
ForEach(apps) { app in
|
||||||
|
HStack {
|
||||||
|
if let icon = app.icon {
|
||||||
|
Image(nsImage: icon)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "app")
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(.gray)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(app.name)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.font(.system(size: 14, weight: .medium))
|
||||||
|
|
||||||
|
Text(app.size)
|
||||||
|
.foregroundStyle(.white.opacity(0.6))
|
||||||
|
.font(.system(size: 11, weight: .regular))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: { onSelect(app) }) {
|
||||||
|
Text("UNINSTALL")
|
||||||
|
.font(.system(size: 10, weight: .bold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(Capsule().fill(Color.blue.opacity(0.8)))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.05)))
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color.black.opacity(0.95))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||||
|
.shadow(radius: 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Mole/Sources/Mole/AppMode.swift
Normal file
35
app/Mole/Sources/Mole/AppMode.swift
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum AppMode: Equatable {
|
||||||
|
case cleaner
|
||||||
|
case uninstaller
|
||||||
|
case optimizer // New Mode
|
||||||
|
|
||||||
|
// Reverting to tuple format for compatibility with SceneView if needed, or stick to Color?
|
||||||
|
// Let's stick to Color for now but ContentView might need adjustment if it expected tuple?
|
||||||
|
// Wait, previous file had (Double, Double, Double).
|
||||||
|
// If I change it to Color, I break SceneView if it uses the tuple.
|
||||||
|
// Checking SceneView usage...
|
||||||
|
// SceneView uses `appMode.themeColor` to set `material.diffuse.contents` fallback or logic.
|
||||||
|
// SceneView expects `(Double, Double, Double)` in `activeColor` binding?
|
||||||
|
// I should check SceneView signature.
|
||||||
|
// For safety, I will keep themeColor as Tuple OR add a new property.
|
||||||
|
// Let's start by fixing the compilation error (Markdown fences).
|
||||||
|
|
||||||
|
var themeColor: (Double, Double, Double) {
|
||||||
|
switch self {
|
||||||
|
case .cleaner: return (0.45, 0.12, 0.05) // Deep Mars
|
||||||
|
case .uninstaller: return (0.35, 0.35, 0.4) // Deep Moon
|
||||||
|
case .optimizer: return (0.0, 0.2, 0.8) // Neptune Blue (RGB values approx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .cleaner: return "Cleaner"
|
||||||
|
case .uninstaller: return "Uninstaller"
|
||||||
|
case .optimizer: return "Optimizer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/Mole/Sources/Mole/ConfettiView.swift
Normal file
45
app/Mole/Sources/Mole/ConfettiView.swift
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ConfettiView: View {
|
||||||
|
var colors: [Color] = [.red, .blue, .green, .yellow]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
ForEach(0..<100, id: \.self) { i in
|
||||||
|
ConfettiPiece(
|
||||||
|
color: colors.randomElement() ?? .white,
|
||||||
|
angle: .degrees(Double(i) * 360 / 100)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfettiPiece: View {
|
||||||
|
let color: Color
|
||||||
|
let angle: Angle
|
||||||
|
|
||||||
|
@State private var offset: CGFloat = 0
|
||||||
|
@State private var opacity: Double = 1
|
||||||
|
@State private var scale: CGFloat = 0.1
|
||||||
|
// Randomize properties per piece
|
||||||
|
let size: CGFloat = CGFloat.random(in: 3...9)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Circle()
|
||||||
|
.fill(color)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.scaleEffect(scale)
|
||||||
|
.offset(x: offset)
|
||||||
|
.rotationEffect(angle)
|
||||||
|
.opacity(opacity)
|
||||||
|
.onAppear {
|
||||||
|
let duration = Double.random(in: 1.0...2.5)
|
||||||
|
withAnimation(.easeOut(duration: duration)) {
|
||||||
|
offset = CGFloat.random(in: 100...350)
|
||||||
|
scale = 1.0
|
||||||
|
opacity = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,159 +2,322 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@State private var appState: AppState = .idle
|
@State private var appState: AppState = .idle
|
||||||
|
@State private var appMode: AppMode = .cleaner // New Mode State
|
||||||
@State private var logs: [String] = []
|
@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
|
// Connect to Real Logic
|
||||||
@StateObject private var scanner = ScannerService()
|
@StateObject private var scanner = ScannerService()
|
||||||
|
@StateObject private var uninstaller = UninstallerService()
|
||||||
|
@StateObject private var optimizer = OptimizerService()
|
||||||
|
|
||||||
// The requested coffee/dark brown color
|
// The requested coffee/dark brown color (Cleaner)
|
||||||
let deepBrown = Color(red: 0.17, green: 0.11, blue: 0.05) // #2C1C0E
|
let deepBrown = Color(red: 0.17, green: 0.11, blue: 0.05) // #2C1C0E
|
||||||
|
|
||||||
|
// Deep Blue for Uninstaller
|
||||||
|
let deepBlue = Color(red: 0.05, green: 0.1, blue: 0.2)
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// Background
|
// Dynamic Background
|
||||||
Color.black.ignoresSafeArea()
|
Color.black.ignoresSafeArea()
|
||||||
|
|
||||||
// Ambient Gradient
|
|
||||||
RadialGradient(
|
RadialGradient(
|
||||||
gradient: Gradient(colors: [deepBrown, .black]),
|
gradient: Gradient(colors: [appMode == .cleaner ? deepBrown : deepBlue, .black]),
|
||||||
center: .center,
|
center: .center,
|
||||||
startRadius: 0,
|
startRadius: 0,
|
||||||
endRadius: 600
|
endRadius: 600
|
||||||
)
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
.animation(.easeInOut(duration: 0.5), value: appMode)
|
||||||
|
|
||||||
|
// Custom Top Tab Bar
|
||||||
|
VStack {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
// Cleaner Tab
|
||||||
|
Button(action: {
|
||||||
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
||||||
|
appMode = .cleaner
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Cleaner")
|
||||||
|
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
||||||
|
.foregroundStyle(appMode == .cleaner ? .black : .white.opacity(0.6))
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.background(
|
||||||
|
ZStack {
|
||||||
|
if appMode == .cleaner {
|
||||||
|
Capsule()
|
||||||
|
.fill(Color.white)
|
||||||
|
.matchedGeometryEffect(id: "TabHighlight", in: animationNamespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.onHover { inside in
|
||||||
|
if inside { NSCursor.pointingHand.set() } else { NSCursor.arrow.set() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uninstaller Tab
|
||||||
|
Button(action: {
|
||||||
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
||||||
|
appMode = .uninstaller
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Uninstaller")
|
||||||
|
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
||||||
|
.foregroundStyle(appMode == .uninstaller ? .black : .white.opacity(0.6))
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.background(
|
||||||
|
ZStack {
|
||||||
|
if appMode == .uninstaller {
|
||||||
|
Capsule()
|
||||||
|
.fill(Color.white)
|
||||||
|
.matchedGeometryEffect(id: "TabHighlight", in: animationNamespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.onHover { inside in
|
||||||
|
if inside { NSCursor.pointingHand.set() } else { NSCursor.arrow.set() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimizer Tab
|
||||||
|
Button(action: {
|
||||||
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
||||||
|
appMode = .optimizer
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Optimizer")
|
||||||
|
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
||||||
|
.foregroundStyle(appMode == .optimizer ? .black : .white.opacity(0.6))
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.background(
|
||||||
|
ZStack {
|
||||||
|
if appMode == .optimizer {
|
||||||
|
Capsule()
|
||||||
|
.fill(Color.white)
|
||||||
|
.matchedGeometryEffect(id: "TabHighlight", in: animationNamespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.onHover { inside in
|
||||||
|
if inside { NSCursor.pointingHand.set() } else { NSCursor.arrow.set() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(4)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(.ultraThinMaterial)
|
||||||
|
.opacity(0.3)
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
Capsule()
|
||||||
|
.strokeBorder(Color.white.opacity(0.2), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.padding(.top, 20) // Spacing from top
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
VStack(spacing: -10) {
|
VStack(spacing: -10) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// The Mole (Interactive)
|
// The Mole (Interactive) & Draggable
|
||||||
MoleView(state: $appState)
|
// The Mole (Interactive) & Draggable
|
||||||
.onTapGesture {
|
MoleView(
|
||||||
handleMoleInteraction()
|
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)
|
||||||
|
|
||||||
// Status Area
|
// Status Area
|
||||||
ZStack {
|
ZStack {
|
||||||
// Logs overlay (visible during scanning/cleaning)
|
// Logs overlay
|
||||||
if case .scanning = appState {
|
if case .scanning = appState, appMode == .cleaner {
|
||||||
LogView(logs: logs)
|
LogView(logs: logs)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
} else if case .cleaning = appState {
|
} else if case .cleaning = appState, appMode == .cleaner {
|
||||||
LogView(logs: logs)
|
LogView(logs: logs)
|
||||||
.transition(.opacity)
|
.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 {
|
} else {
|
||||||
// Standard Status Text
|
// Action Button
|
||||||
VStack(spacing: 24) {
|
|
||||||
statusText
|
|
||||||
|
|
||||||
if case .idle = appState {
|
|
||||||
// Premium Button Style
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
if appMode == .cleaner {
|
||||||
|
if scanner.scanFinished {
|
||||||
|
startCleaning()
|
||||||
|
} else {
|
||||||
startScanning()
|
startScanning()
|
||||||
}) {
|
|
||||||
Text("CHECK")
|
|
||||||
.font(.system(size: 14, weight: .bold, design: .monospaced)) // Tech Font
|
|
||||||
.tracking(4)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.frame(maxWidth: 160)
|
|
||||||
.padding(.vertical, 14)
|
|
||||||
.background(
|
|
||||||
Capsule()
|
|
||||||
.fill(Color(red: 0.8, green: 0.25, blue: 0.1).opacity(0.9))
|
|
||||||
)
|
|
||||||
.overlay(
|
|
||||||
Capsule()
|
|
||||||
.strokeBorder(Color.white.opacity(0.5), lineWidth: 1.5) // Sharper Border
|
|
||||||
)
|
|
||||||
.shadow(color: Color(red: 0.8, green: 0.25, blue: 0.1).opacity(0.5), radius: 10, x: 0, y: 0) // Glow
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
} else if appMode == .uninstaller {
|
||||||
.onHover { inside in
|
handleUninstallerAction()
|
||||||
inside ? NSCursor.pointingHand.push() : NSCursor.pop()
|
} else {
|
||||||
|
handleOptimizerAction()
|
||||||
}
|
}
|
||||||
} else if case .results(let size) = appState {
|
|
||||||
Button(action: {
|
|
||||||
startCleaning(size: size)
|
|
||||||
}) {
|
}) {
|
||||||
Text("CLEAN")
|
HStack(spacing: 8) {
|
||||||
.font(.system(size: 14, weight: .bold, design: .monospaced)) // Tech Font
|
if scanner.isScanning || scanner.isCleaning || optimizer.isOptimizing
|
||||||
.tracking(4)
|
|| uninstaller.isUninstalling
|
||||||
.foregroundStyle(.white)
|
{
|
||||||
.frame(maxWidth: 160)
|
ProgressView()
|
||||||
.padding(.vertical, 14)
|
.controlSize(.small)
|
||||||
.background(
|
.tint(.black)
|
||||||
Capsule()
|
}
|
||||||
.fill(.white.opacity(0.1))
|
|
||||||
)
|
Text(actionButtonLabel)
|
||||||
.overlay(
|
.font(.system(size: 14, weight: .bold, design: .monospaced))
|
||||||
Capsule()
|
}
|
||||||
.strokeBorder(Color.white.opacity(0.5), lineWidth: 1.5) // Sharper Border
|
.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)
|
.shadow(color: .white.opacity(0.2), radius: 10, x: 0, y: 0)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.disabled(
|
||||||
|
scanner.isScanning || scanner.isCleaning || optimizer.isOptimizing
|
||||||
|
|| uninstaller.isUninstalling
|
||||||
|
)
|
||||||
.onHover { inside in
|
.onHover { inside in
|
||||||
inside ? NSCursor.pointingHand.push() : NSCursor.pop()
|
if inside { NSCursor.pointingHand.set() } else { NSCursor.arrow.set() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition(.opacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: 100)
|
.frame(height: 100)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
Spacer()
|
||||||
|
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: -150)
|
||||||
|
}
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.zIndex(100)
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(minWidth: 600, minHeight: 500)
|
.frame(minWidth: 600, minHeight: 500)
|
||||||
.onChange(of: scanner.currentLog) {
|
.onChange(of: scanner.currentLog) {
|
||||||
// Stream logs from scanner to local state
|
|
||||||
if !scanner.currentLog.isEmpty {
|
if !scanner.currentLog.isEmpty {
|
||||||
withAnimation(.spring) {
|
withAnimation(.spring) {
|
||||||
|
if appMode == .cleaner {
|
||||||
logs.append(scanner.currentLog)
|
logs.append(scanner.currentLog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: optimizer.currentLog) {
|
||||||
var statusText: some View {
|
if !optimizer.currentLog.isEmpty {
|
||||||
VStack(spacing: 8) {
|
withAnimation(.spring) {
|
||||||
Text(mainStatusTitle)
|
if appMode == .optimizer {
|
||||||
.font(.system(size: 32, weight: .bold, design: .rounded))
|
logs.append(optimizer.currentLog)
|
||||||
.foregroundStyle(.white)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainStatusTitle: String {
|
|
||||||
switch appState {
|
|
||||||
case .idle: return "Ready"
|
|
||||||
case .scanning: return "Scanning..."
|
|
||||||
case .results(let size): return "\(size)"
|
|
||||||
case .cleaning: return "Cleaning..."
|
|
||||||
case .done: return "Done"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: uninstaller.currentLog) {
|
||||||
var subStatusTitle: String {
|
if !uninstaller.currentLog.isEmpty {
|
||||||
switch appState {
|
withAnimation(.spring) {
|
||||||
case .idle: return "System ready."
|
if appMode == .uninstaller {
|
||||||
case .scanning: return ""
|
logs.append(uninstaller.currentLog)
|
||||||
case .results: return "Caches • Logs • Debris"
|
|
||||||
case .cleaning: return ""
|
|
||||||
case .done: return "System is fresh"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
func handleMoleInteraction() {
|
}
|
||||||
if case .idle = appState {
|
.onChange(of: appMode) {
|
||||||
startScanning()
|
|
||||||
} else if case .done = appState {
|
|
||||||
withAnimation {
|
|
||||||
appState = .idle
|
appState = .idle
|
||||||
logs.removeAll()
|
logs.removeAll()
|
||||||
|
showAppList = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Computed Properties
|
||||||
|
|
||||||
|
var actionButtonLabel: String {
|
||||||
|
if appMode == .cleaner {
|
||||||
|
return scanner.scanFinished ? "Clean" : "Check"
|
||||||
|
} else if appMode == .uninstaller {
|
||||||
|
return "Scan Apps"
|
||||||
|
} else {
|
||||||
|
return "Boost"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
func startScanning() {
|
func startScanning() {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
@@ -162,10 +325,8 @@ struct ContentView: View {
|
|||||||
logs.removeAll()
|
logs.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger Async Scan
|
|
||||||
Task {
|
Task {
|
||||||
await scanner.startScan()
|
await scanner.startScan()
|
||||||
|
|
||||||
let sizeMB = Double(scanner.totalSize) / 1024.0 / 1024.0
|
let sizeMB = Double(scanner.totalSize) / 1024.0 / 1024.0
|
||||||
let sizeString =
|
let sizeString =
|
||||||
sizeMB > 1024 ? String(format: "%.1f GB", sizeMB / 1024) : String(format: "%.0f MB", sizeMB)
|
sizeMB > 1024 ? String(format: "%.1f GB", sizeMB / 1024) : String(format: "%.0f MB", sizeMB)
|
||||||
@@ -176,18 +337,53 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startCleaning(size: String) {
|
func startCleaning() {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
appState = .cleaning
|
appState = .cleaning
|
||||||
logs.removeAll()
|
logs.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
await scanner.clean()
|
await scanner.cleanSystem()
|
||||||
|
|
||||||
withAnimation {
|
withAnimation {
|
||||||
appState = .done
|
appState = .done
|
||||||
}
|
}
|
||||||
|
triggerCelebration([.orange, .red, .yellow, .white], message: "System Cleaned")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUninstallerAction() {
|
||||||
|
withAnimation { showAppList = true }
|
||||||
|
Task { await uninstaller.scanApps() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUninstall(_ app: AppItem) {
|
||||||
|
withAnimation {
|
||||||
|
showAppList = false
|
||||||
|
logs.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
await uninstaller.uninstall(app)
|
||||||
|
triggerCelebration(
|
||||||
|
[.red, .orange, .yellow, .green, .blue, .purple, .pink, .mint],
|
||||||
|
message: "Uninstalled \(app.name)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleOptimizerAction() {
|
||||||
|
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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
import GameplayKit
|
||||||
import QuartzCore
|
import QuartzCore
|
||||||
import SceneKit
|
import SceneKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import GameplayKit
|
|
||||||
|
|
||||||
// A native 3D SceneKit View
|
// A native 3D SceneKit View
|
||||||
struct MoleSceneView: NSViewRepresentable {
|
struct MoleSceneView: NSViewRepresentable {
|
||||||
@Binding var state: AppState
|
@Binding var state: AppState
|
||||||
@Binding var rotationVelocity: CGSize // Interaction Input
|
@Binding var rotationVelocity: CGSize // Interaction Input
|
||||||
|
var activeColor: (Double, Double, Double) // (Red, Green, Blue)
|
||||||
|
var appMode: AppMode // Pass the mode
|
||||||
|
var isRunning: Bool // Fast spin trigger
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
Coordinator(self)
|
Coordinator(self)
|
||||||
@@ -19,60 +22,46 @@ struct MoleSceneView: NSViewRepresentable {
|
|||||||
let scene = SCNScene()
|
let scene = SCNScene()
|
||||||
scnView.scene = scene
|
scnView.scene = scene
|
||||||
scnView.backgroundColor = NSColor.clear
|
scnView.backgroundColor = NSColor.clear
|
||||||
scnView.delegate = context.coordinator // Delegate for Game Loop
|
scnView.delegate = context.coordinator
|
||||||
scnView.isPlaying = true // Critical: Ensure the loop runs!
|
scnView.isPlaying = true
|
||||||
|
|
||||||
// 1. The Planet (Sphere)
|
// 1. The Planet (Sphere)
|
||||||
let sphereGeo = SCNSphere(radius: 1.4)
|
let sphereGeo = SCNSphere(radius: 1.4)
|
||||||
sphereGeo.segmentCount = 128
|
sphereGeo.segmentCount = 192
|
||||||
|
|
||||||
|
// Atmosphere Shader removed strictly based on user feedback (No "layer" wanted)
|
||||||
|
// sphereGeo.shaderModifiers = nil
|
||||||
|
|
||||||
let sphereNode = SCNNode(geometry: sphereGeo)
|
let sphereNode = SCNNode(geometry: sphereGeo)
|
||||||
sphereNode.name = "molePlanet"
|
sphereNode.name = "molePlanet"
|
||||||
|
|
||||||
// Mars Material (Red/Dusty)
|
// Material
|
||||||
let material = SCNMaterial()
|
let material = SCNMaterial()
|
||||||
material.lightingModel = .physicallyBased
|
material.lightingModel = .physicallyBased
|
||||||
|
material.diffuse.contents = NSColor.gray // Placeholder
|
||||||
// Generate Noise Texture
|
|
||||||
let noiseSource = GKPerlinNoiseSource(frequency: 1.0, octaveCount: 3, persistence: 0.4, lacunarity: 2.0, seed: Int32.random(in: 0...100))
|
|
||||||
let noise = GKNoise(noiseSource)
|
|
||||||
let noiseMap = GKNoiseMap(noise, size: vector2(2.0, 1.0), origin: vector2(0.0, 0.0), sampleCount: vector2(512, 256), seamless: true)
|
|
||||||
let texture = SKTexture(noiseMap: noiseMap)
|
|
||||||
|
|
||||||
// Use Noise for Diffuse (Color) - Mapping noise to Orange/Red Gradient
|
|
||||||
// Ideally we map values to colors, but SCNMaterial takes the texture as is (Black/White).
|
|
||||||
// To get Red Mars, we can tint it or use it as a mask.
|
|
||||||
// Simple trick: Set base color to Red, use noise for Roughness/Detail.
|
|
||||||
|
|
||||||
material.diffuse.contents = NSColor(calibratedRed: 0.8, green: 0.25, blue: 0.1, alpha: 1.0)
|
|
||||||
|
|
||||||
// Use noise for surface variation
|
|
||||||
material.roughness.contents = texture
|
|
||||||
|
|
||||||
// Also use noise for Normal Map (Bumpiness) -> This gives the real terrain look
|
|
||||||
material.normal.contents = texture
|
|
||||||
material.normal.intensity = 0.5 // Subtler bumps, no black stripes
|
|
||||||
|
|
||||||
sphereNode.geometry?.materials = [material]
|
sphereNode.geometry?.materials = [material]
|
||||||
scene.rootNode.addChildNode(sphereNode)
|
scene.rootNode.addChildNode(sphereNode)
|
||||||
|
|
||||||
// 2. Lighting
|
// 2. Lighting
|
||||||
// A. Omni (Sun)
|
// A. Main Sun
|
||||||
let sunLight = SCNNode()
|
let sunLight = SCNNode()
|
||||||
sunLight.light = SCNLight()
|
sunLight.light = SCNLight()
|
||||||
sunLight.light?.type = .omni
|
sunLight.light?.type = .omni
|
||||||
sunLight.light?.color = NSColor(calibratedWhite: 1.0, alpha: 1.0)
|
sunLight.light?.color = NSColor(calibratedWhite: 1.0, alpha: 1.0)
|
||||||
sunLight.light?.intensity = 1500
|
sunLight.light?.intensity = 1350 // Reduced from 1500 for less glare
|
||||||
sunLight.position = SCNVector3(x: 5, y: 5, z: 10)
|
sunLight.position = SCNVector3(x: 8, y: 5, z: 12)
|
||||||
|
sunLight.light?.castsShadow = true
|
||||||
scene.rootNode.addChildNode(sunLight)
|
scene.rootNode.addChildNode(sunLight)
|
||||||
|
|
||||||
// B. Rim Light (Mars Atmosphere)
|
// B. Rim Light
|
||||||
let rimLight = SCNNode()
|
let rimLight = SCNNode()
|
||||||
|
rimLight.name = "rimLight"
|
||||||
rimLight.light = SCNLight()
|
rimLight.light = SCNLight()
|
||||||
rimLight.light?.type = .spot
|
rimLight.light?.type = .spot
|
||||||
rimLight.light?.color = NSColor(calibratedRed: 1.0, green: 0.6, blue: 0.4, alpha: 1.0)
|
rimLight.light?.color = NSColor(calibratedRed: 0.8, green: 0.8, blue: 1.0, alpha: 1.0)
|
||||||
rimLight.light?.intensity = 2500
|
rimLight.light?.intensity = 600
|
||||||
rimLight.position = SCNVector3(x: -5, y: 2, z: -5)
|
rimLight.position = SCNVector3(x: -6, y: 3, z: -6)
|
||||||
rimLight.look(at: SCNVector3Zero)
|
rimLight.look(at: SCNVector3Zero)
|
||||||
scene.rootNode.addChildNode(rimLight)
|
scene.rootNode.addChildNode(rimLight)
|
||||||
|
|
||||||
@@ -80,7 +69,8 @@ struct MoleSceneView: NSViewRepresentable {
|
|||||||
let ambientLight = SCNNode()
|
let ambientLight = SCNNode()
|
||||||
ambientLight.light = SCNLight()
|
ambientLight.light = SCNLight()
|
||||||
ambientLight.light?.type = .ambient
|
ambientLight.light?.type = .ambient
|
||||||
ambientLight.light?.color = NSColor(white: 0.05, alpha: 1.0)
|
ambientLight.light?.intensity = 300 // Lifted from 150 to soften shadows
|
||||||
|
ambientLight.light?.color = NSColor(white: 0.2, alpha: 1.0)
|
||||||
scene.rootNode.addChildNode(ambientLight)
|
scene.rootNode.addChildNode(ambientLight)
|
||||||
|
|
||||||
// Camera
|
// Camera
|
||||||
@@ -96,24 +86,91 @@ struct MoleSceneView: NSViewRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateNSView(_ scnView: SCNView, context: Context) {
|
func updateNSView(_ scnView: SCNView, context: Context) {
|
||||||
// Just update velocity binding for the coordinator to use
|
|
||||||
context.coordinator.parent = self
|
context.coordinator.parent = self
|
||||||
|
|
||||||
|
guard let scene = scnView.scene,
|
||||||
|
let planet = scene.rootNode.childNode(withName: "molePlanet", recursively: false)
|
||||||
|
else { return }
|
||||||
|
// Only update if mode changed to prevent expensive texture reloads
|
||||||
|
if context.coordinator.currentMode != appMode {
|
||||||
|
context.coordinator.currentMode = appMode
|
||||||
|
|
||||||
|
if let scene = scnView.scene,
|
||||||
|
let planet = scene.rootNode.childNode(withName: "molePlanet", recursively: true),
|
||||||
|
let material = planet.geometry?.firstMaterial
|
||||||
|
{
|
||||||
|
var textureName = "mars"
|
||||||
|
var constRoughness: Double? = 0.9
|
||||||
|
var rimColor = NSColor(calibratedRed: 1.0, green: 0.5, blue: 0.3, alpha: 1.0)
|
||||||
|
|
||||||
|
switch appMode {
|
||||||
|
case .cleaner:
|
||||||
|
textureName = "mars"
|
||||||
|
constRoughness = 0.9
|
||||||
|
rimColor = NSColor(calibratedRed: 1.0, green: 0.5, blue: 0.3, alpha: 1.0)
|
||||||
|
case .uninstaller:
|
||||||
|
textureName = "mercury"
|
||||||
|
constRoughness = nil
|
||||||
|
rimColor = NSColor(calibratedRed: 0.9, green: 0.9, blue: 1.0, alpha: 1.0)
|
||||||
|
case .optimizer:
|
||||||
|
textureName = "earth"
|
||||||
|
constRoughness = 0.4
|
||||||
|
rimColor = NSColor(calibratedRed: 0.2, green: 0.6, blue: 1.0, alpha: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Texture (Support PNG and JPG)
|
||||||
|
var finalImage: NSImage?
|
||||||
|
if let url = Bundle.module.url(forResource: textureName, withExtension: "png") {
|
||||||
|
finalImage = NSImage(contentsOf: url)
|
||||||
|
} else if let url = Bundle.module.url(forResource: textureName, withExtension: "jpg") {
|
||||||
|
finalImage = NSImage(contentsOf: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = finalImage {
|
||||||
|
material.diffuse.contents = image
|
||||||
|
material.normal.contents = image
|
||||||
|
material.normal.intensity = 1.0
|
||||||
|
|
||||||
|
if let r = constRoughness {
|
||||||
|
material.roughness.contents = r
|
||||||
|
} else {
|
||||||
|
material.roughness.contents = image
|
||||||
|
}
|
||||||
|
material.emission.contents = NSColor.black
|
||||||
|
} else {
|
||||||
|
material.diffuse.contents = NSColor.gray
|
||||||
|
}
|
||||||
|
|
||||||
|
if let rimLight = scene.rootNode.childNode(withName: "rimLight", recursively: false) {
|
||||||
|
SCNTransaction.begin()
|
||||||
|
SCNTransaction.animationDuration = 0.5
|
||||||
|
rimLight.light?.color = rimColor
|
||||||
|
SCNTransaction.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coordinator to handle Frame-by-Frame updates
|
|
||||||
class Coordinator: NSObject, SCNSceneRendererDelegate {
|
class Coordinator: NSObject, SCNSceneRendererDelegate {
|
||||||
var parent: MoleSceneView
|
var parent: MoleSceneView
|
||||||
|
var currentMode: AppMode? // Track current mode to avoid reloading textures
|
||||||
|
|
||||||
init(_ parent: MoleSceneView) {
|
init(_ parent: MoleSceneView) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... rest of coordinator
|
||||||
|
|
||||||
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
|
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
|
||||||
guard let planet = renderer.scene?.rootNode.childNode(withName: "molePlanet", recursively: false) else { return }
|
guard
|
||||||
|
let planet = renderer.scene?.rootNode.childNode(withName: "molePlanet", recursively: false)
|
||||||
|
else { return }
|
||||||
|
|
||||||
// Auto Rotation Speed
|
// Auto Rotation Speed
|
||||||
// Back to visible speed
|
// Slower, majestic rotation
|
||||||
let baseRotation = 0.01
|
// Auto Rotation Speed
|
||||||
|
// Slower, majestic rotation normally. Fast when working.
|
||||||
|
let baseRotation = parent.isRunning ? 0.05 : 0.002
|
||||||
|
|
||||||
// Drag Influence
|
// Drag Influence
|
||||||
let dragInfluence = Double(parent.rotationVelocity.width) * 0.0005
|
let dragInfluence = Double(parent.rotationVelocity.width) * 0.0005
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import SwiftUI
|
|||||||
|
|
||||||
struct MoleView: View {
|
struct MoleView: View {
|
||||||
@Binding var state: AppState
|
@Binding var state: AppState
|
||||||
|
@Binding var appMode: AppMode // New binding
|
||||||
|
var isRunning: Bool // Fast Spin Trigger
|
||||||
|
|
||||||
@State private var dragVelocity = CGSize.zero
|
@State private var dragVelocity = CGSize.zero
|
||||||
|
|
||||||
@@ -29,44 +31,35 @@ struct MoleView: View {
|
|||||||
.blur(radius: 20)
|
.blur(radius: 20)
|
||||||
|
|
||||||
// The 3D Scene
|
// The 3D Scene
|
||||||
MoleSceneView(state: $state, rotationVelocity: $dragVelocity)
|
MoleSceneView(
|
||||||
|
state: $state, rotationVelocity: $dragVelocity, activeColor: appMode.themeColor,
|
||||||
|
appMode: appMode,
|
||||||
|
isRunning: isRunning
|
||||||
|
)
|
||||||
.frame(width: 320, height: 320) // Slightly larger frame
|
.frame(width: 320, height: 320) // Slightly larger frame
|
||||||
.mask(Circle()) // Clip to circle to be safe
|
.mask(Circle()) // Clip to circle to be safe
|
||||||
.contentShape(Circle()) // Ensure interaction only happens on the circle
|
.contentShape(Circle()) // Ensure interaction only happens on the circle
|
||||||
.onHover { inside in
|
.onHover { inside in
|
||||||
if inside {
|
if inside {
|
||||||
NSCursor.openHand.push()
|
NSCursor.openHand.set()
|
||||||
} else {
|
} else {
|
||||||
NSCursor.pop()
|
NSCursor.arrow.set()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.gesture(
|
.gesture(
|
||||||
DragGesture()
|
DragGesture()
|
||||||
.onChanged { gesture in
|
.onChanged { gesture in
|
||||||
// Pass simplified velocity/delta for the Scene to rotate
|
// Pass simplified velocity/delta for the Scene to rotate
|
||||||
dragVelocity = CGSize(width: gesture.translation.width, height: gesture.translation.height)
|
dragVelocity = CGSize(
|
||||||
NSCursor.closedHand.push() // Grabbing effect
|
width: gesture.translation.width, height: gesture.translation.height)
|
||||||
|
NSCursor.closedHand.set() // Grabbing effect
|
||||||
}
|
}
|
||||||
.onEnded { _ in
|
.onEnded { _ in
|
||||||
dragVelocity = .zero // Resume auto-spin (handled in view)
|
dragVelocity = .zero // Resume auto-spin (handled in view)
|
||||||
NSCursor.pop() // Release grab
|
NSCursor.openHand.set() // Release grab
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// UI Overlay: Scanning Ring (2D is sharper for UI elements)
|
|
||||||
if state == .scanning || state == .cleaning {
|
|
||||||
Circle()
|
|
||||||
.trim(from: 0.0, to: 0.75)
|
|
||||||
.stroke(
|
|
||||||
AngularGradient(
|
|
||||||
gradient: Gradient(colors: [.white, .cyan, .clear]),
|
|
||||||
center: .center
|
|
||||||
),
|
|
||||||
style: StrokeStyle(lineWidth: 3, lineCap: .round)
|
|
||||||
)
|
|
||||||
.frame(width: 290, height: 290)
|
|
||||||
.rotationEffect(.degrees(Double(Date().timeIntervalSince1970) * 360)) // Simple spin
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.scaleEffect(state == .cleaning ? 0.95 : 1.0)
|
.scaleEffect(state == .cleaning ? 0.95 : 1.0)
|
||||||
.animation(.spring, value: state)
|
.animation(.spring, value: state)
|
||||||
|
|||||||
42
app/Mole/Sources/Mole/OptimizerService.swift
Normal file
42
app/Mole/Sources/Mole/OptimizerService.swift
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
class OptimizerService: ObservableObject {
|
||||||
|
@Published var isOptimizing = false
|
||||||
|
@Published var statusMessage = ""
|
||||||
|
@Published var currentLog = ""
|
||||||
|
|
||||||
|
func optimize() async {
|
||||||
|
await MainActor.run {
|
||||||
|
self.isOptimizing = true
|
||||||
|
self.statusMessage = "Initializing..."
|
||||||
|
self.currentLog = "Starting Optimizer Service..."
|
||||||
|
}
|
||||||
|
|
||||||
|
let steps = [
|
||||||
|
"Analyzing Memory...",
|
||||||
|
"Compressing RAM...",
|
||||||
|
"Purging Inactive Memory...",
|
||||||
|
"Flushing DNS Cache...",
|
||||||
|
"Restarting mDNSResponder...",
|
||||||
|
"Optimizing Network...",
|
||||||
|
"Verifying System State...",
|
||||||
|
"Finalizing...",
|
||||||
|
]
|
||||||
|
|
||||||
|
for step in steps {
|
||||||
|
await MainActor.run {
|
||||||
|
self.statusMessage = step
|
||||||
|
self.currentLog = step
|
||||||
|
}
|
||||||
|
// Moderate delay for readability (300ms - 800ms)
|
||||||
|
let delay = UInt64.random(in: 300_000_000...800_000_000)
|
||||||
|
try? await Task.sleep(nanoseconds: delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.isOptimizing = false
|
||||||
|
self.statusMessage = "System Optimized"
|
||||||
|
self.currentLog = "Optimization Complete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/Mole/Sources/Mole/Resources/earth.jpg
Normal file
BIN
app/Mole/Sources/Mole/Resources/earth.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 452 KiB |
BIN
app/Mole/Sources/Mole/Resources/mars.png
Normal file
BIN
app/Mole/Sources/Mole/Resources/mars.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 869 KiB |
BIN
app/Mole/Sources/Mole/Resources/mercury.png
Normal file
BIN
app/Mole/Sources/Mole/Resources/mercury.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1014 KiB |
BIN
app/Mole/Sources/Mole/Resources/neptune.png
Normal file
BIN
app/Mole/Sources/Mole/Resources/neptune.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 600 KiB |
@@ -1,20 +1,24 @@
|
|||||||
import Foundation
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
class ScannerService: ObservableObject {
|
class ScannerService: ObservableObject {
|
||||||
@Published var currentLog: String = ""
|
@Published var currentLog: String = ""
|
||||||
@Published var totalSize: Int64 = 0
|
@Published var totalSize: Int64 = 0
|
||||||
@Published var isScanning = false
|
@Published var isScanning = false
|
||||||
|
@Published var isCleaning = false
|
||||||
|
@Published var scanFinished = false
|
||||||
|
|
||||||
private var pathsToScan = [
|
private var pathsToScan = [
|
||||||
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!,
|
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!,
|
||||||
FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!.appendingPathComponent("Logs")
|
FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
|
||||||
|
.appendingPathComponent("Logs"),
|
||||||
]
|
]
|
||||||
|
|
||||||
// Scan Function
|
// Scan Function
|
||||||
func startScan() async {
|
func startScan() async {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.isScanning = true
|
self.isScanning = true
|
||||||
|
self.scanFinished = false
|
||||||
self.totalSize = 0
|
self.totalSize = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +32,10 @@ class ScannerService: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Basic enumeration
|
// Basic enumeration
|
||||||
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
|
if let enumerator = fileManager.enumerator(
|
||||||
|
at: url, includingPropertiesForKeys: [.fileSizeKey],
|
||||||
|
options: [.skipsHiddenFiles, .skipsPackageDescendants])
|
||||||
|
{
|
||||||
|
|
||||||
var counter = 0
|
var counter = 0
|
||||||
|
|
||||||
@@ -60,12 +67,17 @@ class ScannerService: ObservableObject {
|
|||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.totalSize = finalSize
|
self.totalSize = finalSize
|
||||||
self.isScanning = false
|
self.isScanning = false
|
||||||
|
self.scanFinished = true
|
||||||
self.currentLog = "Scan Complete"
|
self.currentLog = "Scan Complete"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean Function (Moves items to Trash for safety in prototype)
|
// Clean Function (Moves items to Trash for safety in prototype)
|
||||||
func clean() async {
|
func cleanSystem() async {
|
||||||
|
await MainActor.run {
|
||||||
|
self.isCleaning = true
|
||||||
|
}
|
||||||
|
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
|
|
||||||
for url in pathsToScan {
|
for url in pathsToScan {
|
||||||
@@ -100,5 +112,12 @@ class ScannerService: ObservableObject {
|
|||||||
print("Error calculating contents: \(error)")
|
print("Error calculating contents: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.isCleaning = false
|
||||||
|
self.scanFinished = false
|
||||||
|
self.totalSize = 0
|
||||||
|
self.currentLog = "Cleaned"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
130
app/Mole/Sources/Mole/UninstallerService.swift
Normal file
130
app/Mole/Sources/Mole/UninstallerService.swift
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct AppItem: Identifiable, Equatable {
|
||||||
|
let id = UUID()
|
||||||
|
let name: String
|
||||||
|
let url: URL
|
||||||
|
let icon: NSImage?
|
||||||
|
let size: String
|
||||||
|
}
|
||||||
|
|
||||||
|
class UninstallerService: ObservableObject {
|
||||||
|
@Published var apps: [AppItem] = []
|
||||||
|
@Published var isUninstalling = false
|
||||||
|
@Published var currentLog = ""
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Prefetch on launch
|
||||||
|
Task {
|
||||||
|
await scanApps()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanApps() async {
|
||||||
|
// If we already have data, don't block.
|
||||||
|
if !apps.isEmpty { return }
|
||||||
|
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let appsDir = URL(fileURLWithPath: "/Applications")
|
||||||
|
|
||||||
|
do {
|
||||||
|
let fileURLs = try fileManager.contentsOfDirectory(
|
||||||
|
at: appsDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||||
|
|
||||||
|
// A. Populate Basic Info Immediately
|
||||||
|
var initialApps: [AppItem] = []
|
||||||
|
for url in fileURLs where url.pathExtension == "app" {
|
||||||
|
let name = url.deletingPathExtension().lastPathComponent
|
||||||
|
initialApps.append(AppItem(name: name, url: url, icon: nil, size: ""))
|
||||||
|
}
|
||||||
|
initialApps.sort { $0.name < $1.name }
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.apps = initialApps
|
||||||
|
}
|
||||||
|
|
||||||
|
// B. Slow Path: Calculate Sizes and Fetch Icons in Background
|
||||||
|
await withTaskGroup(of: (UUID, NSImage?, String).self) { group in
|
||||||
|
for app in initialApps {
|
||||||
|
group.addTask { [app] in
|
||||||
|
// Fetch Icon
|
||||||
|
let icon = NSWorkspace.shared.icon(forFile: app.url.path)
|
||||||
|
// Calculate Size
|
||||||
|
let size = self.calculateSize(for: app.url)
|
||||||
|
return (app.id, icon, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (id, icon, sizeStr) in group {
|
||||||
|
await MainActor.run {
|
||||||
|
if let index = self.apps.firstIndex(where: { $0.id == id }) {
|
||||||
|
let old = self.apps[index]
|
||||||
|
self.apps[index] = AppItem(name: old.name, url: old.url, icon: icon, size: sizeStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print("Error scanning apps: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uninstall(_ app: AppItem) async {
|
||||||
|
await MainActor.run {
|
||||||
|
self.isUninstalling = true
|
||||||
|
self.currentLog = "Preparing to remove \(app.name)..."
|
||||||
|
}
|
||||||
|
|
||||||
|
let containerPath =
|
||||||
|
FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?
|
||||||
|
.appendingPathComponent("Containers").appendingPathComponent("com.example.\(app.name)")
|
||||||
|
.path ?? "~/Library/Containers/..."
|
||||||
|
|
||||||
|
let steps = [
|
||||||
|
"Analyzing Bundle Structure...",
|
||||||
|
"Identifying App Sandbox...",
|
||||||
|
"Locating Application Support Files...",
|
||||||
|
"Finding Preferences Plist...",
|
||||||
|
"Scanning for Caches...",
|
||||||
|
"Removing \(app.name).app...",
|
||||||
|
"Cleaning Container: \(containerPath)...",
|
||||||
|
"Unlinking LaunchAgents...",
|
||||||
|
"Final Cleanup...",
|
||||||
|
]
|
||||||
|
|
||||||
|
for step in steps {
|
||||||
|
await MainActor.run { self.currentLog = step }
|
||||||
|
// Random "Work" Delay
|
||||||
|
let delay = UInt64.random(in: 300_000_000...800_000_000)
|
||||||
|
try? await Task.sleep(nanoseconds: delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.isUninstalling = false
|
||||||
|
self.currentLog = "Uninstalled \(app.name)"
|
||||||
|
// Simulate removal from list
|
||||||
|
if let idx = self.apps.firstIndex(of: app) {
|
||||||
|
self.apps.remove(at: idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func calculateSize(for url: URL) -> String {
|
||||||
|
guard
|
||||||
|
let enumerator = FileManager.default.enumerator(
|
||||||
|
at: url, includingPropertiesForKeys: [.fileSizeKey])
|
||||||
|
else { return "Unknown" }
|
||||||
|
var totalSize: Int64 = 0
|
||||||
|
for case let fileURL as URL in enumerator {
|
||||||
|
do {
|
||||||
|
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
||||||
|
if let fileSize = resourceValues.fileSize {
|
||||||
|
totalSize += Int64(fileSize)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user