diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index 5eab03a..32e0c4f 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + C7045A282B03E71D00376976 /* MiniMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A272B03E71D00376976 /* MiniMode.swift */; }; + C7045A2A2B03FAF000376976 /* TopBarMini.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A292B03FAF000376976 /* TopBarMini.swift */; }; C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; }; C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; }; C7575D382B0181FC006A600A /* FileWatcher in Frameworks */ = {isa = PBXBuildFile; productRef = C7575D372B0181FC006A600A /* FileWatcher */; }; @@ -59,6 +61,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + C7045A272B03E71D00376976 /* MiniMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniMode.swift; sourceTree = ""; }; + C7045A292B03FAF000376976 /* TopBarMini.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBarMini.swift; sourceTree = ""; }; C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; }; C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; @@ -206,9 +210,11 @@ C77B90222AF2D616009CC655 /* FilesView.swift */, C76D08542AF89CDE00D07867 /* AppListView.swift */, C7D31D472AFEB23700C7ED9E /* TopBar.swift */, + C7045A292B03FAF000376976 /* TopBarMini.swift */, C7D31D492AFEB26700C7ED9E /* AppListItems.swift */, C7D31D522B0048BE00C7ED9E /* TrashView.swift */, C7D31D582B00A49300C7ED9E /* DropTarget.swift */, + C7045A272B03E71D00376976 /* MiniMode.swift */, ); path = Views; sourceTree = ""; @@ -336,7 +342,9 @@ C7D31D462AFDA90C00C7ED9E /* Sentinel.swift in Sources */, C7D31D592B00A49300C7ED9E /* DropTarget.swift in Sources */, C77B90062AF18E2E009CC655 /* ContentView.swift in Sources */, + C7045A2A2B03FAF000376976 /* TopBarMini.swift in Sources */, C77B901C2AF193A3009CC655 /* Styles.swift in Sources */, + C7045A282B03E71D00376976 /* MiniMode.swift in Sources */, C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */, C76D084C2AF83C9D00D07867 /* Permissions.swift in Sources */, C76D084A2AF83C6E00D07867 /* General.swift in Sources */, @@ -517,7 +525,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BK8443AXLU; ENABLE_HARDENED_RUNTIME = YES; @@ -534,7 +542,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -551,7 +559,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BK8443AXLU; ENABLE_HARDENED_RUNTIME = YES; @@ -568,7 +576,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Pearcleaner/Logic/AppState.swift b/Pearcleaner/Logic/AppState.swift index 15c9348..7b17ccc 100644 --- a/Pearcleaner/Logic/AppState.swift +++ b/Pearcleaner/Logic/AppState.swift @@ -19,6 +19,7 @@ class AppState: ObservableObject @Published var selectedItems = Set() @Published var alertType = AlertType.off @Published var currentView = CurrentDetailsView.empty +// @Published var currentMode = CurrentMode.regular @Published var showAlert: Bool = false @Published var sidebar: Bool = true @Published var isReminderVisible: Bool = false @@ -74,8 +75,15 @@ enum CurrentDetailsView:Int { case empty case files + case apps } +//enum CurrentMode:Int +//{ +// case regular +// case mini +//} + enum AlertType:Int { case diskAccess diff --git a/Pearcleaner/Logic/DeepLink.swift b/Pearcleaner/Logic/DeepLink.swift index 0949af2..76ff08a 100644 --- a/Pearcleaner/Logic/DeepLink.swift +++ b/Pearcleaner/Logic/DeepLink.swift @@ -17,25 +17,58 @@ class DeeplinkManager { } func manage(url: URL, appState: AppState) { - guard url.scheme == DeepLinkConstants.scheme, - url.host == DeepLinkConstants.host, - let components = URLComponents(url: url, resolvingAgainstBaseURL: true), - let queryItems = components.queryItems - else { - print("URL does not match the expected scheme and host") - return - } - - if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value { - let pathURL = URL(fileURLWithPath: path) - let appInfo = getAppInfo(atPath: pathURL) - updateOnMain { - appState.appInfo = appInfo! - findPathsForApp(appState: appState, appInfo: appState.appInfo) - appState.currentView = .files + if url.pathExtension == "app" { + handleAppBundle(url: url, appState: appState) + } else if url.scheme == DeepLinkConstants.scheme, + url.host == DeepLinkConstants.host, + let components = URLComponents(url: url, resolvingAgainstBaseURL: true), + let queryItems = components.queryItems { + if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value { + let pathURL = URL(fileURLWithPath: path) + let appInfo = getAppInfo(atPath: pathURL) + updateOnMain { + appState.appInfo = appInfo! + findPathsForApp(appState: appState, appInfo: appState.appInfo) + appState.currentView = .files + } + } else { + print("No path query parameter found in the URL") } } else { - print("No path query parameter found in the URL") + print("URL does not match the expected scheme and host") } } + +// func manage(url: URL, appState: AppState) { +// guard url.scheme == DeepLinkConstants.scheme, +// url.host == DeepLinkConstants.host, +// let components = URLComponents(url: url, resolvingAgainstBaseURL: true), +// let queryItems = components.queryItems +// else { +// print("URL does not match the expected scheme and host") +// return +// } +// +// if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value { +// let pathURL = URL(fileURLWithPath: path) +// let appInfo = getAppInfo(atPath: pathURL) +// updateOnMain { +// appState.appInfo = appInfo! +// findPathsForApp(appState: appState, appInfo: appState.appInfo) +// appState.currentView = .files +// } +// } else { +// print("No path query parameter found in the URL") +// } +// } + + func handleAppBundle(url: URL, appState: AppState) { + let appInfo = getAppInfo(atPath: url) + updateOnMain { + appState.appInfo = appInfo! + findPathsForApp(appState: appState, appInfo: appState.appInfo) + appState.currentView = .files + } + } + } diff --git a/Pearcleaner/Logic/Styles.swift b/Pearcleaner/Logic/Styles.swift index 803c379..837f9ef 100644 --- a/Pearcleaner/Logic/Styles.swift +++ b/Pearcleaner/Logic/Styles.swift @@ -28,18 +28,18 @@ struct SimpleButtonStyle: ButtonStyle { .resizable() .scaledToFit() .frame(width: 20) - .foregroundColor(color) + .foregroundColor(hovered ? color : color.opacity(0.5)) } .padding(8) - .background { - if hovered && !(shield ?? false) { -// Circle() +// .background { +// if hovered && !(shield ?? false) { +//// Circle() +//// .strokeBorder(Color("AccentColor"), lineWidth: 1) +// RoundedRectangle(cornerRadius: 8) // .strokeBorder(Color("AccentColor"), lineWidth: 1) - RoundedRectangle(cornerRadius: 8) - .strokeBorder(Color("AccentColor"), lineWidth: 1) - } - - } +// } +// +// } .onHover { hovering in withAnimation() { hovered = hovering diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index dad15f0..6e40fb2 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -24,6 +24,15 @@ func updateOnBackground(_ updates: @escaping () -> Void) { } +// Resize window +func resizeWindow() { + if let window = NSApplication.shared.windows.first { + let newSize = NSSize(width: 500, height: 450) + window.setContentSize(newSize) + } +} + + // Check for access to Full Disk access func checkAndRequestFullDiskAccess(appState: AppState, skipAlert: Bool = false) -> Bool { @AppStorage("settings.permissions.disk") var diskP: Bool = false @@ -268,8 +277,9 @@ func presentAlert(appState: AppState) -> Alert { switch appState.alertType { case .update: - return Alert(title: Text("Update Available 🥳"), message: Text("You may choose to install the update now, otherwise you may start the update later from Settings"), primaryButton: .default(Text("Install")) { - downloadUpdate(appState: appState) + return Alert(title: Text("Update Available 🥳"), message: Text("You may choose to install the update now, otherwise you may check again later from Settings"), primaryButton: .default(Text("Open")) { +// downloadUpdate(appState: appState) + launchUpdate() appState.alertType = .off }, secondaryButton: .cancel()) case .no_update: @@ -278,7 +288,7 @@ func presentAlert(appState: AppState) -> Alert { appState.alertType = .off }) case .diskAccess: - return Alert(title: Text("Permissions"), message: Text("This application requires Full Disk and Accessibility permissions. Once you grant full disk, don't restart the app yet. Wait a second for a second prompt for accessibility and then restart"), primaryButton: .default(Text("Allow in Settings")) { + return Alert(title: Text("Permissions"), message: Text("This application requires Full Disk and Accessibility permissions. Drag the app into the Full Disk pane if not visible."), primaryButton: .default(Text("Allow in Settings")) { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { NSWorkspace.shared.open(url) } diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift index 231a701..41d9c7f 100644 --- a/Pearcleaner/PearcleanerApp.swift +++ b/Pearcleaner/PearcleanerApp.swift @@ -16,54 +16,108 @@ struct PearcleanerApp: App { @AppStorage("settings.permissions.disk") private var diskP: Bool = false @AppStorage("settings.permissions.events") private var diskE: Bool = false @AppStorage("displayMode") var displayMode: DisplayMode = .dark + @AppStorage("settings.general.mini") private var mini: Bool = false @State private var search = "" var body: some Scene { WindowGroup { - AppListView(search: $search) - .environmentObject(appState) - .frame(minWidth: 1020, minHeight: 700) - .preferredColorScheme(displayMode.colorScheme) - .alert(isPresented: $appState.showAlert) { presentAlert(appState: appState) } - .handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*")) - .onAppear { - NSWindow.allowsAutomaticWindowTabbing = false - - // Get Apps - let sortedApps = getSortedApps() - appState.sortedApps.userApps = sortedApps.userApps - appState.sortedApps.systemApps = sortedApps.systemApps - - Task { + if !mini { + AppListView(search: $search) + .environmentObject(appState) + .frame(minWidth: 1020, minHeight: 700) + .preferredColorScheme(displayMode.colorScheme) + .alert(isPresented: $appState.showAlert) { presentAlert(appState: appState) } + .handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*")) + .onAppear { + NSWindow.allowsAutomaticWindowTabbing = false + // Get Apps + let sortedApps = getSortedApps() + appState.sortedApps.userApps = sortedApps.userApps + appState.sortedApps.systemApps = sortedApps.systemApps + + Task { + #if !DEBUG - - // Make sure App Support folder exists in the future if needed for storage - ensureApplicationSupportFolderExists(appState: appState) - - // Check for updates - if diskP && diskE { - loadGithubReleases(appState: appState) - } - - // Check for disk/accessibility permissions - _ = checkAndRequestFullDiskAccess(appState: appState) - - - // TIMERS //////////////////////////////////////////////////////////////////////////////////// - - // Check for app updates every 8 hours or whatever user saved setting. Also refresh autosuggestion list - let updateSeconds = updateTimeframe.daysToSeconds - _ = Timer.scheduledTimer(withTimeInterval: updateSeconds, repeats: true) { _ in - DispatchQueue.main.async { + + // Make sure App Support folder exists in the future if needed for storage + ensureApplicationSupportFolderExists(appState: appState) + let fileManager = FileManager.default + let destinationURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner").appendingPathComponent("Pearcleaner.zip") + try? fileManager.removeItem(at: destinationURL) + + // Check for updates + if diskP && diskE { loadGithubReleases(appState: appState) } - } - + + // Check for disk/accessibility permissions + _ = checkAndRequestFullDiskAccess(appState: appState) + + + // TIMERS //////////////////////////////////////////////////////////////////////////////////// + + // Check for app updates every 8 hours or whatever user saved setting. Also refresh autosuggestion list + let updateSeconds = updateTimeframe.daysToSeconds + _ = Timer.scheduledTimer(withTimeInterval: updateSeconds, repeats: true) { _ in + DispatchQueue.main.async { + loadGithubReleases(appState: appState) + } + } + #endif + } } - } + } else { + MiniMode(search: $search) + .environmentObject(appState) + .frame(minWidth: 500, minHeight: 450) + .preferredColorScheme(displayMode.colorScheme) + .alert(isPresented: $appState.showAlert) { presentAlert(appState: appState) } + .handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*")) + .onAppear { + NSWindow.allowsAutomaticWindowTabbing = false + + // Get Apps + let sortedApps = getSortedApps() + appState.sortedApps.userApps = sortedApps.userApps + appState.sortedApps.systemApps = sortedApps.systemApps + + Task { + +#if !DEBUG + + // Make sure App Support folder exists in the future if needed for storage + ensureApplicationSupportFolderExists(appState: appState) + let fileManager = FileManager.default + let destinationURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner").appendingPathComponent("Pearcleaner.zip") + try? fileManager.removeItem(at: destinationURL) + + // Check for updates + if diskP && diskE { + loadGithubReleases(appState: appState) + } + + // Check for disk/accessibility permissions + _ = checkAndRequestFullDiskAccess(appState: appState) + + + // TIMERS //////////////////////////////////////////////////////////////////////////////////// + + // Check for app updates every 8 hours or whatever user saved setting. Also refresh autosuggestion list + let updateSeconds = updateTimeframe.daysToSeconds + _ = Timer.scheduledTimer(withTimeInterval: updateSeconds, repeats: true) { _ in + DispatchQueue.main.async { + loadGithubReleases(appState: appState) + } + } + +#endif + } + } + } + } .windowStyle(.hiddenTitleBar) .windowToolbarStyle(.unified) @@ -73,6 +127,7 @@ struct PearcleanerApp: App { CommandGroup(replacing: .newItem, addition: { }) } + Settings { SettingsView() .environmentObject(appState) @@ -89,4 +144,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } +// func application(_ sender: NSApplication, open urls: [URL]) { +// for url in urls { +// if url.pathExtension == "app" { +// print("Dropped app path:", url.path) +// } +// } +// } } + diff --git a/Pearcleaner/Resources/Assets.xcassets/logo.imageset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/logo.imageset/Contents.json index 38a858c..3fb7627 100644 --- a/Pearcleaner/Resources/Assets.xcassets/logo.imageset/Contents.json +++ b/Pearcleaner/Resources/Assets.xcassets/logo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "logo_combo.png", + "filename" : "logo_combo_small.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/Pearcleaner/Resources/Assets.xcassets/logo.imageset/logo_combo.png b/Pearcleaner/Resources/Assets.xcassets/logo.imageset/logo_combo.png deleted file mode 100644 index ae575d5..0000000 Binary files a/Pearcleaner/Resources/Assets.xcassets/logo.imageset/logo_combo.png and /dev/null differ diff --git a/Pearcleaner/Resources/Assets.xcassets/logo.imageset/logo_combo_small.png b/Pearcleaner/Resources/Assets.xcassets/logo.imageset/logo_combo_small.png new file mode 100644 index 0000000..8255dc0 Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/logo.imageset/logo_combo_small.png differ diff --git a/Pearcleaner/Resources/Assets.xcassets/pear.imageset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/pear.imageset/Contents.json new file mode 100644 index 0000000..9460316 --- /dev/null +++ b/Pearcleaner/Resources/Assets.xcassets/pear.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "pear_small.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Pearcleaner/Resources/Assets.xcassets/pear.imageset/pear_small.png b/Pearcleaner/Resources/Assets.xcassets/pear.imageset/pear_small.png new file mode 100644 index 0000000..5f4b01f Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/pear.imageset/pear_small.png differ diff --git a/Pearcleaner/Resources/Info.plist b/Pearcleaner/Resources/Info.plist index a6aff2e..0819525 100644 --- a/Pearcleaner/Resources/Info.plist +++ b/Pearcleaner/Resources/Info.plist @@ -2,6 +2,21 @@ + CFBundleDocumentTypes + + + CFBundleTypeName + Application Bundle + LSHandlerRank + Default + CFBundleTypeRole + Viewer + LSItemContentTypes + + com.apple.application-bundle + + + CFBundleURLTypes diff --git a/Pearcleaner/Settings/About.swift b/Pearcleaner/Settings/About.swift index 6edc9dc..47ec869 100644 --- a/Pearcleaner/Settings/About.swift +++ b/Pearcleaner/Settings/About.swift @@ -29,7 +29,7 @@ struct AboutSettingsTab: View { .buttonStyle(SimpleButtonStyle(icon: "cloud", help: "Update application", color: Color("AccentColor"))) Button("") { - NSWorkspace.shared.open(URL(string: "https://github.com/ghosted-labs/Pearcleaner")!) + NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner")!) } .buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View repository", color: Color("AccentColor"))) } diff --git a/Pearcleaner/Settings/General.swift b/Pearcleaner/Settings/General.swift index 8fd7896..788f6fc 100644 --- a/Pearcleaner/Settings/General.swift +++ b/Pearcleaner/Settings/General.swift @@ -11,8 +11,10 @@ import SwiftUI struct GeneralSettingsTab: View { @EnvironmentObject var appState: AppState @AppStorage("settings.general.glass") private var glass: Bool = true -// @AppStorage("settings.general.ants") private var ants: Bool = false - + @AppStorage("settings.general.mini") private var mini: Bool = false + @AppStorage("settings.general.dark") var isDark: Bool = true + @AppStorage("displayMode") var displayMode: DisplayMode = .dark + var body: some View { Form { VStack { @@ -34,30 +36,54 @@ struct GeneralSettingsTab: View { .fill(Color("mode").opacity(0.05)) ) -// HStack { -// VStack(alignment: .leading, spacing: 5) { -// Text("Animation").font(.title2) -// Text("Toggles the marching ants animation on the drop zone") -// .font(.footnote) -// .foregroundStyle(.gray) -// } -// Spacer() -// Toggle(isOn: $ants, label: { -// }) -// .toggleStyle(.switch) -// } -// .padding() -// .background( -// RoundedRectangle(cornerRadius: 8) -// .fill(Color("mode").opacity(0.05)) -// ) + HStack { + VStack(alignment: .leading, spacing: 5) { + Text("Dark Mode").font(.title2) + Text("Toggles between dark and light mode") + .font(.footnote) + .foregroundStyle(.gray) + } + Spacer() + Toggle(isOn: $isDark, label: { + }) + .toggleStyle(.switch) + .onChange(of: isDark) { newValue in + displayMode.colorScheme = newValue ? .dark : .light + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color("mode").opacity(0.05)) + ) + + HStack { + VStack(alignment: .leading, spacing: 5) { + Text("Mini").font(.title2) + Text("Toggles a smaller, unified view with hidden app list") + .font(.footnote) + .foregroundStyle(.gray) + } + Spacer() + Toggle(isOn: $mini, label: { + }) + .toggleStyle(.switch) + .onChange(of: mini) { newVal in + resizeWindow() + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color("mode").opacity(0.05)) + ) Spacer() } } .padding(20) - .frame(width: 400, height: 100) + .frame(width: 400, height: 250) } diff --git a/Pearcleaner/Settings/Update.swift b/Pearcleaner/Settings/Update.swift index 5a5529b..1459542 100644 --- a/Pearcleaner/Settings/Update.swift +++ b/Pearcleaner/Settings/Update.swift @@ -124,6 +124,11 @@ func checkForUpdate(appState: AppState, manual: Bool = false) { } } +func launchUpdate() { + NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!) + +} + func downloadUpdate(appState: AppState) { guard let latestRelease = appState.releases.first else { return } guard let asset = latestRelease.assets.first else { return } @@ -140,7 +145,7 @@ func downloadUpdate(appState: AppState) { do { if fileManager.fileExists(atPath: destinationURL.path) { - try fileManager.removeItem(at: destinationURL) + try? fileManager.removeItem(at: destinationURL) } try fileManager.moveItem(at: localURL, to: destinationURL) diff --git a/Pearcleaner/Views/AppListView.swift b/Pearcleaner/Views/AppListView.swift index 6ae1120..c933619 100644 --- a/Pearcleaner/Views/AppListView.swift +++ b/Pearcleaner/Views/AppListView.swift @@ -53,7 +53,24 @@ struct AppListView: View { VStack(alignment: .center) { VStack(alignment: .center, spacing: 20) { - SearchBar(search: $search) + HStack { + SearchBar(search: $search) + + Button("") { + withAnimation(.easeInOut(duration: 0.5)) { + // Refresh Apps list + reload.toggle() + let sortedApps = getSortedApps() + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + appState.sortedApps.userApps = sortedApps.userApps + appState.sortedApps.systemApps = sortedApps.systemApps + reload.toggle() + } + + } + } + .buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", help: "Refresh app list", color: Color("mode"))) + } } .padding(.horizontal) .padding(.top, 20) @@ -157,20 +174,20 @@ struct AppDetailsEmptyView: View { Spacer() - if appState.isReminderVisible { - Text("􀆔 + Z to undo") - .font(.title2) - .foregroundStyle(Color("AccentColor").opacity(0.5)) - .fontWeight(.medium) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - withAnimation { - appState.isReminderVisible = false - } - } - } - Spacer() - } +// if appState.isReminderVisible { +// Text("􀆔 + Z to undo") +// .font(.title2) +// .foregroundStyle(Color("AccentColor").opacity(0.5)) +// .fontWeight(.medium) +// .onAppear { +// DispatchQueue.main.asyncAfter(deadline: .now() + 2) { +// withAnimation { +// appState.isReminderVisible = false +// } +// } +// } +// Spacer() +// } Text("Drop an app above or select one from the list to begin") .font(.title3) diff --git a/Pearcleaner/Views/DropTarget.swift b/Pearcleaner/Views/DropTarget.swift index b2e0f10..16c1427 100644 --- a/Pearcleaner/Views/DropTarget.swift +++ b/Pearcleaner/Views/DropTarget.swift @@ -11,6 +11,7 @@ import UniformTypeIdentifiers struct DropTarget: View, DropDelegate { @AppStorage("settings.general.ants") private var ants: Bool = false + @AppStorage("settings.general.mini") private var mini: Bool = false @ObservedObject var appState: AppState @State private var shouldFlash = false private var types: [UTType] = [.fileURL] @@ -34,15 +35,16 @@ struct DropTarget: View, DropDelegate { ZStack { LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing) - .mask(Image("logo") + .mask( + Image(mini ? "pear" : "logo") .resizable() - .scaledToFit()) + .scaledToFit() + .frame(width: mini ? 104 : 500) + ) } - .frame(width: 500, height: 200) Spacer() - } Spacer() @@ -71,6 +73,8 @@ struct DropTarget: View, DropDelegate { } .onDrop(of: types, delegate: self) .padding(20) + .frame(maxWidth: mini ? 200 : .infinity) + .frame(maxHeight: mini ? 200 : .infinity) } diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift index a1917c5..554468f 100644 --- a/Pearcleaner/Views/FilesView.swift +++ b/Pearcleaner/Views/FilesView.swift @@ -14,6 +14,7 @@ struct FilesView: View { @State private var showDetails: Bool = false @State private var showPop: Bool = false @State private var itemDetails: [(size: String, icon: Image?)] = [] + @AppStorage("settings.general.mini") private var mini: Bool = false @Environment(\.colorScheme) var colorScheme var body: some View { @@ -34,8 +35,9 @@ struct FilesView: View { if let appIcon = appState.appInfo.appIcon { Image(nsImage: appIcon) .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 100, height: 100) + .scaledToFit() +// .aspectRatio(contentMode: .fit) + .frame(width: 50, height: 50) .padding(.leading) } //app title, size and items @@ -43,19 +45,19 @@ struct FilesView: View { HStack(alignment: .center) { VStack(alignment: .leading, spacing: 5){ HStack { - Text("\(appState.appInfo.appName)").font(.title).fontWeight(.bold) + Text("\(appState.appInfo.appName)").font(mini ? .title2 :.title).fontWeight(.bold) // .foregroundStyle(Color("AccentColor")) Text("•").foregroundStyle(Color("AccentColor")) Text("\(appState.appInfo.appVersion)").font(.title3) // .foregroundStyle(.gray.opacity(0.8)) } - Text("\(appState.appInfo.bundleIdentifier)").font(.title3) + Text("\(appState.appInfo.bundleIdentifier)").font(mini ? .footnote :.title3) .foregroundStyle((.gray.opacity(0.8))) } Spacer() - Text("\(appSize)").font(.title).fontWeight(.bold) + Text("\(appSize)").font(mini ? .title2 :.title).fontWeight(.bold) // .foregroundStyle(Color("AccentColor")) } @@ -180,7 +182,7 @@ struct FilesView: View { } } - .frame(minWidth: 700) +// .frame(minWidth: 700) .onAppear { Task { calculateFileDetails() @@ -291,7 +293,7 @@ struct FileDetailsItem: View { Button("") { NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) } - .buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder", color: Color("AccentColor"))) + .buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder", color: Color("mode"))) } .background( diff --git a/Pearcleaner/Views/MiniMode.swift b/Pearcleaner/Views/MiniMode.swift new file mode 100644 index 0000000..d488c67 --- /dev/null +++ b/Pearcleaner/Views/MiniMode.swift @@ -0,0 +1,203 @@ +// +// MiniMode.swift +// Pearcleaner +// +// Created by Alin Lupascu on 11/14/23. +// + + +import Foundation +import SwiftUI + +struct MiniMode: View { + @EnvironmentObject var appState: AppState + @Binding var search: String + @State private var showSys: Bool = true + @State private var showUsr: Bool = true + @State private var reload: Bool = false + + + + var body: some View { + + HStack(alignment: .center, spacing: 0) { + + // Main Mini View + VStack(spacing: 0) { + if appState.currentView == .empty { + TopBarMini(reload: $reload, search: $search) + AppDetailsEmptyView() + } else if appState.currentView == .files { + TopBarMini(reload: $reload, search: $search) + FilesView() + .id(appState.appInfo.id) + } else if appState.currentView == .apps { + TopBarMini(reload: $reload, search: $search) + MiniAppView(search: $search, reload: $reload) + } + } + // .padding(.leading, appState.sidebar ? 0 : 10) + .transition(.move(edge: .leading)) + + } + .edgesIgnoringSafeArea(.all) + .onOpenURL(perform: { url in + let deeplinkManager = DeeplinkManager() + deeplinkManager.manage(url: url, appState: appState) + }) + // MARK: Background for whole app + // .background(Color("bg").opacity(1)) + // .background(VisualEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all)) + } +} + + + + + + +struct MiniEmptyView: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var appState: AppState + @State private var animateGradient: Bool = false + @AppStorage("settings.general.mini") private var mini: Bool = false + + var body: some View { + VStack() { + + Spacer() + + DropTarget(appState: appState) + .frame(maxWidth: mini ? 300 : 500) + + Spacer() + + if appState.isReminderVisible { + Text("CMD + Z to undo") + .font(.title2) + .foregroundStyle(Color("AccentColor").opacity(0.5)) + .fontWeight(.medium) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation { + appState.isReminderVisible = false + } + } + } + Spacer() + } + + Text("Drop an app above or select one from the list to begin") + .font(.title3) + .padding(.bottom, 25) + .opacity(0.5) + } + } +} + + + + + +struct MiniAppView: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var appState: AppState + @State private var animateGradient: Bool = false + @Binding var search: String + @State private var showSys: Bool = true + @State private var showUsr: Bool = true + @Binding var reload: Bool + @AppStorage("settings.general.mini") private var mini: Bool = false + + var body: some View { + + var filteredUserApps: [AppInfo] { + if search.isEmpty { + return appState.sortedApps.userApps + } else { + return appState.sortedApps.userApps.filter { $0.appName.localizedCaseInsensitiveContains(search) } + } + } + + var filteredSystemApps: [AppInfo] { + if search.isEmpty { + return appState.sortedApps.systemApps + } else { + return appState.sortedApps.systemApps.filter { $0.appName.localizedCaseInsensitiveContains(search) } + } + } + + ZStack { + HStack(spacing: 0){ + + if reload { + VStack { + Spacer() + ProgressView("Refreshing applications") + Spacer() + } + .padding(.vertical) + } else { + VStack(alignment: .center) { + +// VStack(alignment: .center, spacing: 20) { +// HStack { +// SearchBar(search: $search) +// } +// } +// .padding(.horizontal, 5) +// .padding(.bottom) + + ScrollView { + + VStack(alignment: .leading) { + + if filteredUserApps.count > 0 { + VStack { + Header(title: "User", count: filteredUserApps.count) + ForEach(filteredUserApps, id: \.self) { appInfo in + AppListItems(appInfo: appInfo) + if appInfo != filteredUserApps.last { + Divider().padding(.horizontal, 5) + } + } + .padding(.bottom) + } + + } + + if filteredSystemApps.count > 0 { + VStack { + Header(title: "System", count: filteredSystemApps.count) + ForEach(filteredSystemApps, id: \.self) { appInfo in + AppListItems(appInfo: appInfo) + if appInfo != filteredSystemApps.last { + Divider().padding(.horizontal, 5) + } + } + } + } + + } + .padding(.horizontal) + + } + .scrollIndicators(.never) + + } + .padding(.vertical) + } + + + } + } + .transition(.move(edge: .leading)) + } +} + + + + + + + diff --git a/Pearcleaner/Views/TopBar.swift b/Pearcleaner/Views/TopBar.swift index 9137f45..ff46d72 100644 --- a/Pearcleaner/Views/TopBar.swift +++ b/Pearcleaner/Views/TopBar.swift @@ -9,57 +9,19 @@ import Foundation import SwiftUI struct TopBar: View { -// @Binding var sidebar: Bool + // @Binding var sidebar: Bool @Binding var reload: Bool @AppStorage("displayMode") var displayMode: DisplayMode = .system @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false + @AppStorage("settings.general.mini") private var mini: Bool = false @EnvironmentObject var appState: AppState - + var body: some View { HStack(alignment: .center, spacing: 20) { - HStack(alignment: .center, spacing: 5) { -// Button("") { -// withAnimation(.easeInOut(duration: 0.5)) { -// appState.sidebar.toggle() -// if appState.sidebar { -// appState.winWidth += 300 -// } else { -// appState.winWidth -= 300 -// } -// } -// } -// .buttonStyle(SimpleButtonStyle(icon: "sidebar.left", help: "Toggle Sidebar", color: Color("AccentColor"))) - - - Button("") { - withAnimation(.easeInOut(duration: 0.5)) { - // Refresh Apps list - reload.toggle() - let sortedApps = getSortedApps() - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - appState.sortedApps.userApps = sortedApps.userApps - appState.sortedApps.systemApps = sortedApps.systemApps - reload.toggle() - } - - } - } - .buttonStyle(SimpleButtonStyle(icon: "arrow.circlepath", help: "Refresh app list", color: Color("AccentColor"))) - - Button("") { - withAnimation(.easeInOut(duration: 0.5)) { - openTrash() - } - } - .buttonStyle(SimpleButtonStyle(icon: "trash", help: "Open Trash", color: Color("AccentColor"))) - } - Spacer() - - if appState.currentView != .empty { Button("") { withAnimation(.easeInOut(duration: 0.5)) { @@ -67,48 +29,38 @@ struct TopBar: View { appState.appInfo = AppInfo.empty } } - .buttonStyle(SimpleButtonStyle(icon: "house", help: "Home", color: Color("AccentColor"))) + .buttonStyle(SimpleButtonStyle(icon: "arrow.down.app", help: "Drop", color: Color("mode"))) } - - + if appState.isReminderVisible { + Text("CMD + Z to undo") + .font(.title2) + .foregroundStyle(Color("AccentColor").opacity(0.5)) + .fontWeight(.medium) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation { + appState.isReminderVisible = false + } + } + } + } Spacer() - HStack(alignment: .center, spacing: 5) { - - if sentinel { - Button("") { - // - } - .buttonStyle(SimpleButtonStyle(icon: "lock.shield", help: "Sentinel enabled", color: .green, shield: true)) - } - - - Button("") { - withAnimation(.easeInOut(duration: 0.5)) { - glass.toggle() - } - } - .buttonStyle(SimpleButtonStyle(icon: glass ? "circle.dashed" : "circle.dashed.inset.filled", help: "Toggle transparency", color: Color("AccentColor"))) - - Button("") { - withAnimation(.easeInOut(duration: 0.5)) { - if displayMode.colorScheme == .dark { - displayMode.colorScheme = .light - } else if displayMode.colorScheme == .light { - displayMode.colorScheme = .dark - } else { - displayMode.colorScheme = .dark - } - } - } - .buttonStyle(SimpleButtonStyle(icon: displayMode.colorScheme == .dark ? "sun.max.fill" : "moon.fill", help: "Toggle appearance", color: Color("AccentColor"))) - } - + + +// if sentinel { +// Button("") { +// // +// } +// .buttonStyle(SimpleButtonStyle(icon: "lock.shield", help: "Sentinel enabled", color: .green, shield: true)) +// } + + } .padding(.horizontal, 20) - .padding(.top, 30) + .padding(.top, 20) } } diff --git a/Pearcleaner/Views/TopBarMini.swift b/Pearcleaner/Views/TopBarMini.swift new file mode 100644 index 0000000..cc04e62 --- /dev/null +++ b/Pearcleaner/Views/TopBarMini.swift @@ -0,0 +1,111 @@ +// +// TopBarMini.swift +// Pearcleaner +// +// Created by Alin Lupascu on 11/14/23. +// + +import Foundation +import SwiftUI + +struct TopBarMini: View { + @Binding var reload: Bool + @AppStorage("displayMode") var displayMode: DisplayMode = .system + @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false + @AppStorage("settings.general.mini") private var mini: Bool = false + @Binding var search: String + @EnvironmentObject var appState: AppState + + var body: some View { + HStack(alignment: .center, spacing: 5) { + Spacer() + if appState.currentView == .apps { + HStack(alignment: .center, spacing: 0) { + Button("") { + withAnimation(.easeInOut(duration: 0.5)) { + appState.currentView = .empty + appState.appInfo = AppInfo.empty + } + } + .buttonStyle(SimpleButtonStyle(icon: "arrow.down.app", help: "Drop", color: Color("mode"))) + + + } + + } else if appState.currentView == .empty { + Button("") { + withAnimation(.easeInOut(duration: 0.5)) { + appState.currentView = .apps + } + } + .buttonStyle(SimpleButtonStyle(icon: "list.triangle", help: "Apps", color: Color("mode"))) + } else if appState.currentView == .files { + Button("") { + withAnimation(.easeInOut(duration: 0.5)) { + appState.currentView = .empty + appState.appInfo = AppInfo.empty + } + } + .buttonStyle(SimpleButtonStyle(icon: "arrow.down.app", help: "Drop", color: Color("mode"))) + Button("") { + withAnimation(.easeInOut(duration: 0.5)) { + appState.currentView = .apps + } + } + .buttonStyle(SimpleButtonStyle(icon: "list.triangle", help: "Apps", color: Color("mode"))) + } + + if appState.currentView == .apps { + HStack { + Spacer() + + SearchBarMini(search: $search) + + Spacer() + + Button("") { + withAnimation(.easeInOut(duration: 0.5)) { + // Refresh Apps list + reload.toggle() + let sortedApps = getSortedApps() + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + appState.sortedApps.userApps = sortedApps.userApps + appState.sortedApps.systemApps = sortedApps.systemApps + reload.toggle() + } + + } + } + .buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", help: "Refresh app list", color: Color("mode"))) + } + } + + + Spacer() + +// if sentinel { +// Button("") { +// // +// } +// .buttonStyle(SimpleButtonStyle(icon: "lock.shield", help: "Sentinel enabled", color: .green, shield: true)) +// } + + } + .edgesIgnoringSafeArea(.all) + .padding(.horizontal) + .padding(.top, 30) + } +} + + +struct SearchBarMini: View { + @Binding var search: String + + var body: some View { + HStack { + TextField("Search", text: $search) + .textFieldStyle(SimpleSearchStyle(trash: true, text: $search)) + } + .frame(height: 30) + } +}