This commit is contained in:
Alin
2024-07-15 17:58:21 -06:00
parent 15c5ad33ae
commit cf5143f17d
40 changed files with 1035 additions and 2614 deletions
+16 -17
View File
@@ -3,13 +3,12 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 56; objectVersion = 60;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
C7045A282B03E71D00376976 /* MiniMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A272B03E71D00376976 /* MiniMode.swift */; }; C7045A282B03E71D00376976 /* MiniMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A272B03E71D00376976 /* MiniMode.swift */; };
C7045A322B068AD700376976 /* NewWin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A312B068AD700376976 /* NewWin.swift */; }; C7045A322B068AD700376976 /* NewWin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A312B068AD700376976 /* NewWin.swift */; };
C7045A342B068B2900376976 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A332B068B2900376976 /* UpdateView.swift */; };
C7045A362B068DD700376976 /* PermView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A352B068DD700376976 /* PermView.swift */; }; C7045A362B068DD700376976 /* PermView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A352B068DD700376976 /* PermView.swift */; };
C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71848432B8D2D600046CB13 /* ZombieView.swift */; }; C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71848432B8D2D600046CB13 /* ZombieView.swift */; };
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; }; C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
@@ -17,12 +16,12 @@
C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */; }; C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */; };
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; }; C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; }; C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; };
C75347642BE599FA00AAFCC3 /* PermissionChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */; };
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; }; C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; }; C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; };
C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; }; C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; };
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* RegularMode.swift */; }; C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* RegularMode.swift */; };
C76EDCD62C45DC82000CF29D /* AlinFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = C76EDCD52C45DC82000CF29D /* AlinFoundation */; };
C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */; }; C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */; };
C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; }; C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; };
C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77B90072AF18E2F009CC655 /* Assets.xcassets */; }; C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77B90072AF18E2F009CC655 /* Assets.xcassets */; };
@@ -35,7 +34,6 @@
C78121662BC892A000BE06BD /* FinderOpen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78121652BC892A000BE06BD /* FinderOpen.swift */; }; C78121662BC892A000BE06BD /* FinderOpen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78121652BC892A000BE06BD /* FinderOpen.swift */; };
C781216B2BC892A000BE06BD /* FinderOpen.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C78121632BC892A000BE06BD /* FinderOpen.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; C781216B2BC892A000BE06BD /* FinderOpen.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C78121632BC892A000BE06BD /* FinderOpen.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
C79947962C34A794007279CC /* Tips.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79947952C34A794007279CC /* Tips.swift */; }; C79947962C34A794007279CC /* Tips.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79947952C34A794007279CC /* Tips.swift */; };
C79D05202BC99EB40083F976 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79D051F2BC99EB40083F976 /* ThemeManager.swift */; };
C7A27E812AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist in Resources */ = {isa = PBXBuildFile; fileRef = C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */; }; C7A27E812AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist in Resources */ = {isa = PBXBuildFile; fileRef = C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */; };
C7A2AE5D2B9644F300161540 /* FileWatcher in Frameworks */ = {isa = PBXBuildFile; productRef = C7A2AE5C2B9644F300161540 /* FileWatcher */; }; C7A2AE5D2B9644F300161540 /* FileWatcher in Frameworks */ = {isa = PBXBuildFile; productRef = C7A2AE5C2B9644F300161540 /* FileWatcher */; };
C7A9CE472B89164700EB6E78 /* Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A9CE462B89164700EB6E78 /* Authorization.swift */; }; C7A9CE472B89164700EB6E78 /* Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A9CE462B89164700EB6E78 /* Authorization.swift */; };
@@ -48,7 +46,6 @@
C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */; }; C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */; };
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */; }; C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */; };
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49EF2BABA14400CCBA16 /* Folders.swift */; }; C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49EF2BABA14400CCBA16 /* Folders.swift */; };
C7DE17652BE54F4900197715 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE17642BE54F4900197715 /* Updater.swift */; };
C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */; }; C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */; };
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */; }; C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */; };
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7ED86B02BA8A35500F13DE4 /* Interface.swift */; }; C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7ED86B02BA8A35500F13DE4 /* Interface.swift */; };
@@ -104,7 +101,6 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
C7045A272B03E71D00376976 /* MiniMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniMode.swift; sourceTree = "<group>"; }; C7045A272B03E71D00376976 /* MiniMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniMode.swift; sourceTree = "<group>"; };
C7045A312B068AD700376976 /* NewWin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewWin.swift; sourceTree = "<group>"; }; C7045A312B068AD700376976 /* NewWin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewWin.swift; sourceTree = "<group>"; };
C7045A332B068B2900376976 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
C7045A352B068DD700376976 /* PermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermView.swift; sourceTree = "<group>"; }; C7045A352B068DD700376976 /* PermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermView.swift; sourceTree = "<group>"; };
C71848432B8D2D600046CB13 /* ZombieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZombieView.swift; sourceTree = "<group>"; }; C71848432B8D2D600046CB13 /* ZombieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZombieView.swift; sourceTree = "<group>"; };
C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = "<group>"; }; C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = "<group>"; };
@@ -113,7 +109,6 @@
C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPathsFetch-Async.swift"; sourceTree = "<group>"; }; C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPathsFetch-Async.swift"; sourceTree = "<group>"; };
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; }; C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; }; C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; };
C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionChecker.swift; sourceTree = "<group>"; };
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; }; C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; }; C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; }; C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
@@ -136,7 +131,6 @@
C78121682BC892A000BE06BD /* FinderOpen.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FinderOpen.entitlements; sourceTree = "<group>"; }; C78121682BC892A000BE06BD /* FinderOpen.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FinderOpen.entitlements; sourceTree = "<group>"; };
C786F1A72C29E599001B3800 /* Builds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Builds; sourceTree = SOURCE_ROOT; }; C786F1A72C29E599001B3800 /* Builds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Builds; sourceTree = SOURCE_ROOT; };
C79947952C34A794007279CC /* Tips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tips.swift; sourceTree = "<group>"; }; C79947952C34A794007279CC /* Tips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tips.swift; sourceTree = "<group>"; };
C79D051F2BC99EB40083F976 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PearcleanerSentinel.entitlements; sourceTree = "<group>"; }; C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PearcleanerSentinel.entitlements; sourceTree = "<group>"; };
C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.alienator88.PearcleanerSentinel.plist; sourceTree = "<group>"; }; C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.alienator88.PearcleanerSentinel.plist; sourceTree = "<group>"; };
C7A9CE462B89164700EB6E78 /* Authorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authorization.swift; sourceTree = "<group>"; }; C7A9CE462B89164700EB6E78 /* Authorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authorization.swift; sourceTree = "<group>"; };
@@ -149,7 +143,6 @@
C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoFetch.swift; sourceTree = "<group>"; }; C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoFetch.swift; sourceTree = "<group>"; };
C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversePathsFetch.swift; sourceTree = "<group>"; }; C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversePathsFetch.swift; sourceTree = "<group>"; };
C7DD49EF2BABA14400CCBA16 /* Folders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folders.swift; sourceTree = "<group>"; }; C7DD49EF2BABA14400CCBA16 /* Folders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folders.swift; sourceTree = "<group>"; };
C7DE17642BE54F4900197715 /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = "<group>"; };
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItem.swift; sourceTree = "<group>"; }; C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItem.swift; sourceTree = "<group>"; };
C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionBuilderView.swift; sourceTree = "<group>"; }; C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionBuilderView.swift; sourceTree = "<group>"; };
C7ED86B02BA8A35500F13DE4 /* Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = "<group>"; }; C7ED86B02BA8A35500F13DE4 /* Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = "<group>"; };
@@ -172,6 +165,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C76EDCD62C45DC82000CF29D /* AlinFoundation in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -189,7 +183,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C7045A312B068AD700376976 /* NewWin.swift */, C7045A312B068AD700376976 /* NewWin.swift */,
C7045A332B068B2900376976 /* UpdateView.swift */,
C7D15BBD2BE85F4D007D57CF /* FeatureView.swift */, C7D15BBD2BE85F4D007D57CF /* FeatureView.swift */,
C7045A352B068DD700376976 /* PermView.swift */, C7045A352B068DD700376976 /* PermView.swift */,
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */, C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */,
@@ -216,9 +209,6 @@
C7A9CE462B89164700EB6E78 /* Authorization.swift */, C7A9CE462B89164700EB6E78 /* Authorization.swift */,
C7ACADE52B92A737000B5845 /* Features.swift */, C7ACADE52B92A737000B5845 /* Features.swift */,
C7ED86B22BA8ED8800F13DE4 /* Trash.swift */, C7ED86B22BA8ED8800F13DE4 /* Trash.swift */,
C79D051F2BC99EB40083F976 /* ThemeManager.swift */,
C7DE17642BE54F4900197715 /* Updater.swift */,
C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */,
); );
path = Logic; path = Logic;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -374,6 +364,7 @@
); );
name = Pearcleaner; name = Pearcleaner;
packageProductDependencies = ( packageProductDependencies = (
C76EDCD52C45DC82000CF29D /* AlinFoundation */,
); );
productName = PeakPurge; productName = PeakPurge;
productReference = C77B90002AF18E2E009CC655 /* Pearcleaner.app */; productReference = C77B90002AF18E2E009CC655 /* Pearcleaner.app */;
@@ -428,6 +419,7 @@
mainGroup = C77B8FF72AF18E2E009CC655; mainGroup = C77B8FF72AF18E2E009CC655;
packageReferences = ( packageReferences = (
C7A2AE5B2B9644DA00161540 /* XCRemoteSwiftPackageReference "FileWatcher" */, C7A2AE5B2B9644DA00161540 /* XCRemoteSwiftPackageReference "FileWatcher" */,
C76EDCD42C45DC82000CF29D /* XCLocalSwiftPackageReference "../AlinFoundation" */,
); );
productRefGroup = C77B90012AF18E2E009CC655 /* Products */; productRefGroup = C77B90012AF18E2E009CC655 /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -477,10 +469,7 @@
C79947962C34A794007279CC /* Tips.swift in Sources */, C79947962C34A794007279CC /* Tips.swift in Sources */,
C76D08482AF83C3F00D07867 /* Update.swift in Sources */, C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */, C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
C7045A342B068B2900376976 /* UpdateView.swift in Sources */,
C77B90162AF19377009CC655 /* AppState.swift in Sources */, C77B90162AF19377009CC655 /* AppState.swift in Sources */,
C75347642BE599FA00AAFCC3 /* PermissionChecker.swift in Sources */,
C7DE17652BE54F4900197715 /* Updater.swift in Sources */,
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */, C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
C7045A322B068AD700376976 /* NewWin.swift in Sources */, C7045A322B068AD700376976 /* NewWin.swift in Sources */,
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */, C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */,
@@ -504,7 +493,6 @@
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */, C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */, C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */,
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */, C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */,
C79D05202BC99EB40083F976 /* ThemeManager.swift in Sources */,
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */, C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */,
C7ACADE62B92A737000B5845 /* Features.swift in Sources */, C7ACADE62B92A737000B5845 /* Features.swift in Sources */,
C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */, C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */,
@@ -905,6 +893,13 @@
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
C76EDCD42C45DC82000CF29D /* XCLocalSwiftPackageReference "../AlinFoundation" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../AlinFoundation;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
C7A2AE5B2B9644DA00161540 /* XCRemoteSwiftPackageReference "FileWatcher" */ = { C7A2AE5B2B9644DA00161540 /* XCRemoteSwiftPackageReference "FileWatcher" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
@@ -917,6 +912,10 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
C76EDCD52C45DC82000CF29D /* AlinFoundation */ = {
isa = XCSwiftPackageProductDependency;
productName = AlinFoundation;
};
C7A2AE5C2B9644F300161540 /* FileWatcher */ = { C7A2AE5C2B9644F300161540 /* FileWatcher */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = C7A2AE5B2B9644DA00161540 /* XCRemoteSwiftPackageReference "FileWatcher" */; package = C7A2AE5B2B9644DA00161540 /* XCRemoteSwiftPackageReference "FileWatcher" */;
@@ -1,4 +1,5 @@
{ {
"originHash" : "2070782a951fcb2eef783fefb42bac8d9dafc68491164378649d1525b43cbc83",
"pins" : [ "pins" : [
{ {
"identity" : "filewatcher", "identity" : "filewatcher",
@@ -10,5 +11,5 @@
} }
} }
], ],
"version" : 2 "version" : 3
} }
+10 -2
View File
@@ -6,17 +6,22 @@
// //
import SwiftUI import SwiftUI
import AlinFoundation
struct AppCommands: Commands { struct AppCommands: Commands {
let appState: AppState let appState: AppState
let locations: Locations let locations: Locations
let fsm: FolderSettingsManager let fsm: FolderSettingsManager
let updater: Updater
let themeManager: ThemeManager
init(appState: AppState, locations: Locations, fsm: FolderSettingsManager) { init(appState: AppState, locations: Locations, fsm: FolderSettingsManager, updater: Updater, themeManager: ThemeManager) {
self.appState = appState self.appState = appState
self.locations = locations self.locations = locations
self.fsm = fsm self.fsm = fsm
self.updater = updater
self.themeManager = themeManager
} }
var body: some Commands { var body: some Commands {
@@ -25,7 +30,7 @@ struct AppCommands: Commands {
CommandGroup(replacing: .appInfo) { CommandGroup(replacing: .appInfo) {
Button { Button {
loadGithubReleases(appState: appState, manual: true) updater.checkForUpdates()
} label: { } label: {
Text("Check for Updates") Text("Check for Updates")
} }
@@ -55,12 +60,15 @@ struct AppCommands: Commands {
Button Button
{ {
if appState.currentView != .zombie {
undoTrash(appState: appState) { undoTrash(appState: appState) {
reloadAppsList(appState: appState, fsm: fsm) reloadAppsList(appState: appState, fsm: fsm)
for app in appState.trashedFiles { for app in appState.trashedFiles {
AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths() AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths()
} }
} }
}
} label: { } label: {
Label("Undo Removal", systemImage: "clear") Label("Undo Removal", systemImage: "clear")
} }
+1
View File
@@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
class AppInfoFetcher { class AppInfoFetcher {
static let fileManager = FileManager.default static let fileManager = FileManager.default
+1 -1
View File
@@ -8,7 +8,7 @@
import Foundation import Foundation
import AppKit import AppKit
import SwiftUI import SwiftUI
import AlinFoundation
class AppPathFinder { class AppPathFinder {
private var appInfo: AppInfo private var appInfo: AppInfo
+3 -6
View File
@@ -22,15 +22,14 @@ class AppState: ObservableObject {
@Published var currentView = CurrentDetailsView.empty @Published var currentView = CurrentDetailsView.empty
@Published var showAlert: Bool = false @Published var showAlert: Bool = false
@Published var sidebar: Bool = true @Published var sidebar: Bool = true
@Published var releases = [Release]()
@Published var progressBar: (String, Double) = ("Ready", 0.0) @Published var progressBar: (String, Double) = ("Ready", 0.0)
@Published var reload: Bool = false @Published var reload: Bool = false
@Published var showProgress: Bool = false @Published var showProgress: Bool = false
@Published var finderExtensionEnabled: Bool = false @Published var finderExtensionEnabled: Bool = false
@Published var updateAvailable: Bool = false // @Published var updateAvailable: Bool = false
@Published var featureAvailable: Bool = false @Published var featureAvailable: Bool = false
@Published var permissionsOkay: Bool = true // @Published var permissionsOkay: Bool = true
@Published var permissionResults: PermissionsCheckResults? // @Published var permissionResults: PermissionsCheckResults?
@Published var showUninstallAlert: Bool = false @Published var showUninstallAlert: Bool = false
@Published var oneShotMode: Bool = false @Published var oneShotMode: Bool = false
@Published var showConditionBuilder: Bool = false @Published var showConditionBuilder: Bool = false
@@ -182,8 +181,6 @@ enum CurrentDetailsView:Int
enum NewWindow:Int enum NewWindow:Int
{ {
case update
case no_update
case perm case perm
case feature case feature
} }
+1
View File
@@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
class DeeplinkManager { class DeeplinkManager {
@Binding var showPopover: Bool @Binding var showPopover: Bool
+4 -2
View File
@@ -7,6 +7,8 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
func getFeatures(appState: AppState, features: Binding<String>) { func getFeatures(appState: AppState, features: Binding<String>) {
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/contents/features.json")! let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/contents/features.json")!
@@ -78,7 +80,7 @@ struct FeatureNotificationView: View {
.foregroundStyle(.white) .foregroundStyle(.white)
Text("Show") Text("Check")
.foregroundStyle(.white) .foregroundStyle(.white)
} }
@@ -103,7 +105,7 @@ struct FeatureNotificationView: View {
} }
.frame(height: 30) .frame(height: 30)
.padding(5) .padding(5)
.background(Color("mode").opacity(0.05)) .background(.primary.opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 6)) .clipShape(RoundedRectangle(cornerRadius: 6))
.padding(.horizontal) .padding(.horizontal)
.padding(.bottom) .padding(.bottom)
+1 -1
View File
@@ -26,7 +26,7 @@ class Locations: ObservableObject {
self.tempDir = tempDir self.tempDir = tempDir
self.apps = Category(name: "Apps", paths: [ self.apps = Category(name: "Apps", paths: [
"\(home)", // "\(home)", //MARK: Disable searching for files in user's home folder
"\(home)/Library", "\(home)/Library",
"\(home)/Library/Application Scripts", "\(home)/Library/Application Scripts",
"\(home)/Library/Application Support", "\(home)/Library/Application Support",
+17 -16
View File
@@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
// Get all apps from /Applications and ~/Applications // Get all apps from /Applications and ~/Applications
@@ -212,14 +213,14 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
if let scriptObject = NSAppleScript(source: scriptSource) { if let scriptObject = NSAppleScript(source: scriptSource) {
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error) let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
if let error = error { if let error = error {
checkAllPermissions(appState: appState) { results in // checkAllPermissions(appState: appState) { results in
appState.permissionResults = results // appState.permissionResults = results
if !results.allPermissionsGranted { // if !results.allPermissionsGranted {
updateOnMain { // updateOnMain {
appState.permissionsOkay = false // appState.permissionsOkay = false
} // }
} // }
} // }
printOS("Trash Error: \(error)") printOS("Trash Error: \(error)")
DispatchQueue.main.async { DispatchQueue.main.async {
completion(false) // Indicate failure completion(false) // Indicate failure
@@ -261,14 +262,14 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
if let scriptObject = NSAppleScript(source: scriptSource) { if let scriptObject = NSAppleScript(source: scriptSource) {
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error) let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
if let error = error { if let error = error {
checkAllPermissions(appState: appState) { results in // checkAllPermissions(appState: appState) { results in
appState.permissionResults = results // appState.permissionResults = results
if !results.allPermissionsGranted { // if !results.allPermissionsGranted {
updateOnMain { // updateOnMain {
appState.permissionsOkay = false // appState.permissionsOkay = false
} // }
} // }
} // }
printOS("Undo Trash Error: \(error)") printOS("Undo Trash Error: \(error)")
} else if let outputString = output.stringValue { } else if let outputString = output.stringValue {
printOS(outputString) printOS(outputString)
-176
View File
@@ -1,176 +0,0 @@
//
// PermissionChecker.swift
// Pearcleaner
//
// Created by Alin Lupascu on 5/3/24.
//
import SwiftUI
//import EventKit
struct PermissionsCheckResults {
var fullDiskAccess: Bool
var accessibility: Bool
var automation: Bool
// Computed property to check if all permissions are granted
var allPermissionsGranted: Bool {
return fullDiskAccess && accessibility && automation
}
}
func checkAllPermissions(appState: AppState, completion: @escaping (PermissionsCheckResults) -> Void) {
let dispatchGroup = DispatchGroup()
// Check Full Disk Access
var fullDiskAccess = false
@AppStorage("settings.permissions.hasLaunched") var hasLaunched: Bool = false
let process = Process()
process.launchPath = "/usr/bin/sqlite3"
process.arguments = ["/Library/Application Support/com.apple.TCC/TCC.db", "select client from access where auth_value and service = 'kTCCServiceSystemPolicyAllFiles' and client = 'com.alienator88.Pearcleaner'"]
let pipe = Pipe()
process.standardOutput = pipe
process.launch()
process.waitUntilExit() // Ensure process completes
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
fullDiskAccess = (output?.contains("com.alienator88.Pearcleaner") ?? false)
// Check Accessibility
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
let accessibilityEnabled = AXIsProcessTrustedWithOptions(options)
// Check Automation Permission
var automationAccess = false
dispatchGroup.enter()
checkAutomationPermission(appState: appState) { success in
automationAccess = success
dispatchGroup.leave()
}
// Wait for all async checks to complete
dispatchGroup.notify(queue: .main) {
let results = PermissionsCheckResults(
fullDiskAccess: fullDiskAccess,
accessibility: accessibilityEnabled,
automation: automationAccess
)
// Check if any permission is denied and show window
if !(results.fullDiskAccess && results.accessibility && results.automation) {
updateOnMain {
appState.permissionsOkay = false
}
}
completion(results)
}
}
// Check Finder Automation permission
func checkAutomationPermission(appState: AppState, completion: @escaping (Bool) -> Void) {
DispatchQueue.global(qos: .background).async {
let scriptText = "tell application \"Finder\" to return name of home"
var error: NSDictionary?
if let script = NSAppleScript(source: scriptText) {
let _ = script.executeAndReturnError(&error)
DispatchQueue.main.async {
completion(error == nil)
}
} else {
DispatchQueue.main.async {
completion(false)
}
}
}
}
struct PermissionsNotificationView: View {
let appState: AppState
@State private var hovered: Bool = false
var body: some View {
HStack {
Text("Missing Permissions!")
.font(.callout)
.opacity(0.5)
.padding(.leading, 7)
Spacer()
settingsLinkButton
}
.frame(height: 30)
.padding(5)
.background(Color("mode").opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 6))
.padding(.horizontal)
.padding(.bottom)
}
@ViewBuilder
private var settingsLinkButton: some View {
if #available(macOS 14.0, *) {
SettingsLink {
labelContent
}
.buttonStyle(PlainButtonStyle())
.padding(4)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 6))
.onHover { hover in
withAnimation {
hovered = hover
}
}
.help("Check all permissions")
} else {
Button(action: {
if #available(macOS 13.0, *) {
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
}
else {
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
}) {
labelContent
}
.buttonStyle(PlainButtonStyle())
.padding(4)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 6))
.onHover { hover in
withAnimation {
hovered = hover
}
}
.help("Check all permissions")
}
}
@ViewBuilder
private var labelContent: some View {
HStack(alignment: .center) {
Image(systemName: !hovered ? "lock" : "lock.fill")
.resizable()
.scaledToFit()
.frame(width: 14, height: 14)
.foregroundStyle(.white)
Text("Check")
.foregroundStyle(.white)
}
.padding(3)
}
}
@@ -7,6 +7,7 @@
import Foundation import Foundation
import AppKit import AppKit
import AlinFoundation
class ReversePathsSearcher { class ReversePathsSearcher {
private let appState: AppState private let appState: AppState
+17 -651
View File
@@ -7,88 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
struct SimpleButtonStyle: ButtonStyle {
@State private var hovered = false
let icon: String
let iconFlip: String
let label: String
let help: String
let color: Color
let size: CGFloat
let padding: CGFloat
let rotate: Bool
init(icon: String, iconFlip: String = "", label: String = "", help: String, color: Color = Color("mode"), size: CGFloat = 20, padding: CGFloat = 5, rotate: Bool = false) {
self.icon = icon
self.iconFlip = iconFlip
self.label = label
self.help = help
self.color = color
self.size = size
self.padding = padding
self.rotate = rotate
}
func makeBody(configuration: Self.Configuration) -> some View {
HStack(alignment: .center) {
Image(systemName: (hovered && !iconFlip.isEmpty) ? iconFlip : icon)
.resizable()
.scaledToFit()
.frame(width: size, height: size)
.scaleEffect(hovered ? 1.1 : 1.0)
.rotationEffect(.degrees(rotate ? (hovered ? 90 : 0) : 0))
.animation(.easeInOut(duration: 0.2), value: hovered)
if !label.isEmpty {
Text(label)
}
}
.foregroundColor(hovered ? color : color.opacity(0.5))
.padding(padding)
.onHover { hovering in
withAnimation() {
hovered = hovering
}
}
.scaleEffect(configuration.isPressed ? 0.95 : 1)
.help(help)
}
}
struct RegularButtonStyle: ButtonStyle {
let icon: String
let label: String
let help: String
@State private var hovered = false
func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .center) {
Image(systemName: icon).foregroundColor(Color("mode")).padding(.leading, 2).padding(.trailing, 0)
Text(label).textCase(.uppercase).foregroundColor(Color("mode")).fontWeight(.bold).padding(.trailing, 2)
}
.padding(8)
.background(!configuration.isPressed ? hovered ? Color("mode").opacity(0.4) : Color("mode").opacity(0) : Color("mode").opacity(0.5))
.cornerRadius(6)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
.onHover { hovering in
withAnimation(Animation.easeIn(duration: 0.15)) {
hovered = hovering
}
}
.overlay(
RoundedRectangle(cornerRadius: 6)
.strokeBorder(Color("mode").opacity(0.5), lineWidth: 1)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
)
.help(help)
}
}
struct ResetSettingsButtonStyle: ButtonStyle { struct ResetSettingsButtonStyle: ButtonStyle {
@Binding var isResetting: Bool @Binding var isResetting: Bool
@@ -105,13 +24,13 @@ struct ResetSettingsButtonStyle: ButtonStyle {
.frame(width: 15) .frame(width: 15)
} else { } else {
Image(systemName: "gear") Image(systemName: "gear")
.foregroundColor(Color("mode")).padding(.leading, 2).padding(.trailing, 0) .foregroundColor(.primary).padding(.leading, 2).padding(.trailing, 0)
} }
Text(label) Text(label)
.textCase(.uppercase).foregroundColor(Color("mode")).fontWeight(.bold).padding(.trailing, 2) .textCase(.uppercase).foregroundColor(.primary).fontWeight(.bold).padding(.trailing, 2)
} }
.padding(8) .padding(8)
.background(!configuration.isPressed ? hovered ? Color("mode").opacity(0.4) : Color("mode").opacity(0) : Color("mode").opacity(0.5)) .background(!configuration.isPressed ? hovered ? Color.primary.opacity(0.4) : Color.primary.opacity(0) : Color.primary.opacity(0.5))
.cornerRadius(6) .cornerRadius(6)
.scaleEffect(configuration.isPressed ? 0.95 : 1) .scaleEffect(configuration.isPressed ? 0.95 : 1)
.animation(.easeOut(duration: 0.2), value: configuration.isPressed) .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
@@ -122,7 +41,7 @@ struct ResetSettingsButtonStyle: ButtonStyle {
} }
.overlay( .overlay(
RoundedRectangle(cornerRadius: 6) RoundedRectangle(cornerRadius: 6)
.strokeBorder(Color("mode").opacity(0.5), lineWidth: 1) .strokeBorder(.primary.opacity(0.5), lineWidth: 1)
.scaleEffect(configuration.isPressed ? 0.95 : 1) .scaleEffect(configuration.isPressed ? 0.95 : 1)
) )
.help(help) .help(help)
@@ -131,18 +50,18 @@ struct ResetSettingsButtonStyle: ButtonStyle {
struct SimpleCheckboxToggleStyle: ToggleStyle { struct SimpleCheckboxToggleStyle: ToggleStyle {
@EnvironmentObject private var themeSettings: ThemeSettings @EnvironmentObject private var themeManager: ThemeManager
@State private var isHovered: Bool = false @State private var isHovered: Bool = false
func makeBody(configuration: Configuration) -> some View { func makeBody(configuration: Configuration) -> some View {
HStack { HStack {
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
.fill(themeSettings.themeColor.darker(by: 5)) .fill(themeManager.pickerColor.adjustBrightness(5))
.frame(width: 18, height: 18) .frame(width: 18, height: 18)
.overlay { .overlay {
if configuration.isOn { if configuration.isOn {
Image(systemName: "checkmark") Image(systemName: "checkmark")
.foregroundStyle(Color("mode")) .foregroundStyle(.primary)
.scaleEffect(isHovered ? 0.8 : 1.0) .scaleEffect(isHovered ? 0.8 : 1.0)
} }
} }
@@ -160,150 +79,6 @@ struct SimpleCheckboxToggleStyle: ToggleStyle {
} }
struct InfoButton: View {
@State private var isPopoverPresented: Bool = false
let text: String
let color: Color
let label: String
let warning: Bool
let edge: Edge
init(text: String, color: Color = Color("mode"), label: String = "", warning: Bool = false, edge: Edge = .bottom) {
self.text = text
self.color = color
self.label = label
self.warning = warning
self.edge = edge
}
var body: some View {
Button(action: {
self.isPopoverPresented.toggle()
}) {
HStack(alignment: .center, spacing: 5) {
Image(systemName: !warning ? "info.circle.fill" : "exclamationmark.triangle.fill")
.resizable()
.scaledToFit()
.frame(width: 14, height: 14)
.foregroundColor(!warning ? color.opacity(0.7) : color)
.frame(height: 16)
if !label.isEmpty {
Text(label)
.font(.callout)
.foregroundColor(color.opacity(0.7))
}
}
}
.buttonStyle(PlainButtonStyle())
.onHover { isHovered in
if isHovered {
NSCursor.pointingHand.push()
} else {
NSCursor.pop()
}
}
.popover(isPresented: $isPopoverPresented, arrowEdge: edge) {
VStack {
Spacer()
Text(text)
.font(.callout)
.frame(maxWidth: .infinity)
.padding()
Spacer()
}
.frame(width: 300)
}
.padding(.horizontal, 5)
}
}
struct InfoButtonPerms: View {
@State private var isPopoverPresented: Bool = false
let color: Color
let label: String
let warning: Bool
init(color: Color = Color("mode"), label: String = "", warning: Bool = false) {
self.color = color
self.label = label
self.warning = warning
}
var body: some View {
Button(action: {
self.isPopoverPresented.toggle()
}) {
HStack(alignment: .center, spacing: 5) {
Image(systemName: !warning ? "info.circle.fill" : "exclamationmark.triangle.fill")
.resizable()
.scaledToFit()
.frame(width: 14, height: 14)
.foregroundColor(!warning ? color.opacity(0.7) : color)
.frame(height: 16)
if !label.isEmpty {
Text(label)
.font(.callout)
.foregroundColor(color.opacity(0.7))
}
}
}
.buttonStyle(PlainButtonStyle())
.onHover { isHovered in
if isHovered {
NSCursor.pointingHand.push()
} else {
NSCursor.pop()
}
}
.popover(isPresented: $isPopoverPresented, arrowEdge: .bottom) {
VStack(alignment: .leading, spacing: 15) {
HStack(alignment: .top, spacing: 20) {
Image(systemName: "externaldrive")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
Text("Full Disk permission to access files/folders in system paths")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
HStack(alignment: .top, spacing: 20) {
Image(systemName: "accessibility")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
Text("Accessibility permission to allow execution of AppleScript")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
HStack(alignment: .top, spacing: 20) {
Image(systemName: "gearshape.2")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
Text("Automation permission to perform delete actions via Finder")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
}
.padding()
}
.padding(.horizontal, 5)
}
}
struct UninstallButton: ButtonStyle { struct UninstallButton: ButtonStyle {
@State private var hovered: Bool = false @State private var hovered: Bool = false
@@ -371,40 +146,6 @@ struct RescanButton: ButtonStyle {
struct WarningPopoverView: View {
var label: String
var bodyText: String
@Binding var isPresented: Bool
var body: some View {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
.popover(isPresented: $isPresented, arrowEdge: .top) {
VStack {
Spacer()
Text(bodyText)
.font(.callout)
.frame(maxWidth: .infinity)
.padding()
Spacer()
}
.frame(width: 300)
}
Text(label)
.foregroundStyle(Color.red)
Spacer()
}
.onTapGesture {
isPresented.toggle()
}
}
}
public struct NewFeatureView: View { public struct NewFeatureView: View {
var text: String var text: String
var mini: Bool var mini: Bool
@@ -425,7 +166,7 @@ public struct NewFeatureView: View {
showFeature = false showFeature = false
} }
} }
.buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", help: "Close", color: (Color("mode").opacity(0.5)))) .buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", help: "Close", color: (.primary.opacity(0.5))))
.onHover { isHovered in .onHover { isHovered in
if isHovered { if isHovered {
NSCursor.pointingHand.push() NSCursor.pointingHand.push()
@@ -451,7 +192,7 @@ public struct NewFeatureView: View {
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 0.6) .strokeBorder(.primary.opacity(0.2), lineWidth: 0.6)
) )
.padding() .padding()
} }
@@ -459,7 +200,7 @@ public struct NewFeatureView: View {
public struct SlideableDivider: View { public struct SlideableDivider: View {
@EnvironmentObject private var themeSettings: ThemeSettings @EnvironmentObject private var themeManager: ThemeManager
@Binding var dimension: Double @Binding var dimension: Double
@State private var dimensionStart: Double? @State private var dimensionStart: Double?
@State private var handleWidth: Double = 4 @State private var handleWidth: Double = 4
@@ -471,7 +212,7 @@ public struct SlideableDivider: View {
public var body: some View { public var body: some View {
Rectangle() Rectangle()
.foregroundStyle(Color("mode").opacity(0.0)) .foregroundStyle(.primary.opacity(0.0))
.frame(width: 10) .frame(width: 10)
.onHover { inside in .onHover { inside in
if inside { if inside {
@@ -518,117 +259,6 @@ public struct SlideableDivider: View {
} }
struct LabeledDivider: View {
let label: String
@EnvironmentObject var appState: AppState
var body: some View {
HStack(spacing: 0) {
Rectangle()
.frame(height: 1)
.foregroundColor(Color("mode").opacity(0.2))
Text(label)
.textCase(.uppercase)
.font(.title2)
.foregroundColor(Color("mode").opacity(0.6))
.padding(.horizontal, 10)
.frame(minWidth: 80)
Rectangle()
.frame(height: 1)
.foregroundColor(Color("mode").opacity(0.2))
}
.frame(minHeight: 35)
}
}
struct AnimatedSearchStyle: TextFieldStyle {
@State private var isHovered = false
@FocusState private var isFocused: Bool
@Binding var text: String
@EnvironmentObject var appState: AppState
@EnvironmentObject var fsm: FolderSettingsManager
func _body(configuration: TextField<Self._Label>) -> some View {
HStack(spacing: 5) {
if isHovered || !text.isEmpty {
configuration
.frame(height: 15)
.font(.title3)
.textFieldStyle(PlainTextFieldStyle())
.padding(.leading, 50)
Image(systemName: "arrow.triangle.2.circlepath")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 15, height: 15)
.padding(.trailing, 5)
.foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5))
.onTapGesture {
withAnimation {
reloadAppsList(appState: appState, fsm: fsm)
}
}
.help("Refresh app list")
if text != "" {
Image(systemName: "xmark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 10, height: 10)
.padding(.trailing, 5)
.foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5))
.onTapGesture {
withAnimation {
text = ""
}
}
.help("Clear search")
}
} else {
HStack {
Spacer()
Image(systemName: "magnifyingglass")
.resizable()
.scaledToFit()
.frame(width: 18, height: 18)
.foregroundColor(isHovered ? Color("mode") : Color("mode").opacity(0.5))
}
.frame(width: 150)
}
}
.padding(8)
.overlay(
Group {
if isHovered || !text.isEmpty {
RoundedRectangle(cornerRadius: 6)
.strokeBorder(Color("mode").opacity(0.4), lineWidth: 1)
.allowsHitTesting(false)
.padding(.leading, 50)
}
}
)
.onHover { hovering in
withAnimation(Animation.easeInOut(duration: 0.4)) {
self.isHovered = hovering
self.isFocused = hovering
}
}
.focused($isFocused)
}
}
struct RoundedTextFieldStyle: TextFieldStyle { struct RoundedTextFieldStyle: TextFieldStyle {
func _body(configuration: TextField<Self._Label>) -> some View { func _body(configuration: TextField<Self._Label>) -> some View {
@@ -638,7 +268,7 @@ struct RoundedTextFieldStyle: TextFieldStyle {
.textFieldStyle(.plain) .textFieldStyle(.plain)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.strokeBorder(Color("mode").opacity(0.4), lineWidth: 0.8) .strokeBorder(.primary.opacity(0.4), lineWidth: 0.8)
) )
} }
} }
@@ -661,54 +291,6 @@ struct GlassEffect: NSViewRepresentable {
struct VerticalDivider: View {
var color: Color = Color("AccentColor")
var width: CGFloat = 1
var height: CGFloat = 20
var opacity: Double = 1
var body: some View {
Divider()
.foregroundColor(color)
.frame(width: width, height: height)
.opacity(opacity)
.ignoresSafeArea(.all)
}
}
struct WindowActionButton: ButtonStyle {
enum UserAction {
case accept
case cancel
}
@State private var hovered = false
let action: UserAction
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(width: 100)
.padding(10)
.background(
!configuration.isPressed ?
!hovered ?
action == .accept ? Color("button") : Color.red :
action == .accept ? Color("button").opacity(0.8) : Color.red.opacity(0.8) :
action == .accept ? Color("button").opacity(0.5) : Color.red.opacity(0.5)
)
.foregroundColor(.white)
.cornerRadius(8)
.onHover { hovering in
withAnimation(Animation.easeIn(duration: 0.15)) {
hovered = hovering
}
}
}
}
struct SimpleButtonBrightStyle: ButtonStyle { struct SimpleButtonBrightStyle: ButtonStyle {
@State private var hovered = false @State private var hovered = false
@@ -748,165 +330,6 @@ struct SimpleButtonBrightStyle: ButtonStyle {
struct FilesViewActionButton: ButtonStyle {
enum UserAction {
case uninstall
case close
}
@State private var hovered = false
let action: UserAction
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(width: 70)
.padding(6)
.background(
RoundedRectangle(cornerRadius: 6)
.strokeBorder(
configuration.isPressed ?
Color("AccentColor").opacity(0.7) :
hovered ? Color("AccentColor").opacity(0.8) : Color("AccentColor").opacity(0.9), lineWidth: 2
)
)
.foregroundColor(
action == .uninstall ? Color("AccentColor") : Color("mode")
)
.cornerRadius(6)
.onHover { hovering in
withAnimation(Animation.easeIn(duration: 0.15)) {
hovered = hovering
}
}
}
}
struct SentinelToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
ZStack{
RoundedRectangle(cornerRadius: 50)
.fill(configuration.isOn ? greenBG : redBG)
.frame(width: 70, height: 40)
HStack{
Image(systemName: "lock.shield")
.resizable()
.scaledToFit()
.frame(width: 26, height: 26)
.padding(.leading, 10)
.foregroundColor(.white)
Spacer()
Image(systemName: "shield")
.resizable()
.scaledToFit()
.frame(width: 26, height: 26)
.padding(.trailing, 10)
.foregroundColor(.white)
}
}
.frame(width: 70, height: 40, alignment: .center)
.overlay(
Circle()
.foregroundColor(.white)
.shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 0)
.padding(.all, 4)
.offset(x: configuration.isOn ? 15 : -15, y: 0)
)
.overlay(
Image(systemName: "power")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundColor(.black.opacity(0.3))
.offset(x: configuration.isOn ? 15 : -15, y: 0)
)
.onTapGesture {
withAnimation(.easeInOut(duration: 0.5)) {
configuration.isOn.toggle()
}
}
.animation(.default, value: configuration.isOn)
}
}
}
private let greenBG: AnyShapeStyle = AnyShapeStyle(
.green.shadow(.inner(radius: 2, x: 0, y: 1))
)
private let redBG: AnyShapeStyle = AnyShapeStyle(
.red.shadow(.inner(radius: 2, x: 0, y: 1))
)
struct PillPicker: View {
@Binding var index: Int
var onTapAction: () -> Void
@Environment(\.colorScheme) var colorScheme
var body: some View {
HStack(alignment: .center, spacing: 10) {
ForEach(0..<3) { i in
ZStack {
Capsule()
.fill(Color.white)
.offset(x: (CGFloat(index - i) * 100))
.animation(.default, value: index)
.opacity(index == i ? 1 : 0)
.scaleEffect(index == i ? 1 : 0.70)
.shadow(color: Color.black.opacity(0.2), radius: 2, x: 0, y: 2)
HStack(spacing: 0) {
Text(i == 0 ? "Apps" : (i == 1 ? "Widgets" : "Plugins"))
.font(.system(size: 12))
.foregroundColor(index == i ? (colorScheme == .dark ? .black : Color("AccentColor")) : Color("mode"))
}
.padding(5)
.frame(height: 25)
.background(.white.opacity(0.0001))
}
.onTapGesture {
withAnimation {
index = i
onTapAction()
}
}
}
}
.frame(height: 25)
.padding(5)
.background(
Capsule()
.fill(colorScheme == .dark ? .white.opacity(0.1) : .black.opacity(0.06))
.overlay(
Capsule()
.stroke(Color.black.opacity(colorScheme == .dark ? 0.8 : 0.2), lineWidth: 1)
.blur(radius: 1)
.offset(y: 1)
)
)
.clipShape(Capsule())
}
}
struct PearDropView: View { struct PearDropView: View {
var body: some View { var body: some View {
@@ -914,9 +337,9 @@ struct PearDropView: View {
HStack(spacing: 0) { HStack(spacing: 0) {
LinearGradient(gradient: Gradient(colors: [.green, .orange]), startPoint: .leading, endPoint: .trailing) LinearGradient(gradient: Gradient(colors: [.green, .orange]), startPoint: .leading, endPoint: .trailing)
.frame(width: 220) .frame(width: 220)
LinearGradient(gradient: Gradient(colors: [.orange, Color("mode").opacity(0.5)]), startPoint: .leading, endPoint: .trailing) LinearGradient(gradient: Gradient(colors: [.orange, .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
.frame(width: 10) .frame(width: 10)
LinearGradient(gradient: Gradient(colors: [Color("mode").opacity(0.5), Color("mode").opacity(0.5)]), startPoint: .leading, endPoint: .trailing) LinearGradient(gradient: Gradient(colors: [.primary.opacity(0.5), .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
.frame(width: 300) .frame(width: 300)
} }
.mask( .mask(
@@ -934,71 +357,14 @@ struct PearDropView: View {
} }
// Used for testing UI bounds
extension View {
func bounds() -> some View {
self.border(Color.red, width: 1)
}
}
// Background color/glass setter // Background color/glass setter
@ViewBuilder @ViewBuilder
func backgroundView(themeSettings: ThemeSettings, darker: Bool = false, glass: Bool = false) -> some View { func backgroundView(themeManager: ThemeManager = ThemeManager.shared, darker: Bool = false, glass: Bool = false) -> some View {
if glass { if glass {
GlassEffect(material: .sidebar, blendingMode: .behindWindow) GlassEffect(material: .sidebar, blendingMode: .behindWindow)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
} else { } else {
darker ? themeSettings.themeColor.darker(by: 5).edgesIgnoringSafeArea(.all) : themeSettings.themeColor.edgesIgnoringSafeArea(.all) darker ? themeManager.pickerColor.adjustBrightness(5).edgesIgnoringSafeArea(.all) : themeManager.pickerColor.edgesIgnoringSafeArea(.all)
} }
} }
struct PickerModifier: ViewModifier {
@State private var isHovered: Bool = false
let themeSettings: ThemeSettings
func body(content: Content) -> some View {
content
.buttonStyle(.borderless)
.padding(4)
// .background {
// backgroundView(themeSettings: themeSettings, darker: isHovered)
// .clipShape(RoundedRectangle(cornerRadius: 10))
// }
.onHover { hovering in
isHovered = hovering
}
}
}
extension View {
func pickerStyle(themeSettings: ThemeSettings) -> some View {
self.modifier(PickerModifier(themeSettings: themeSettings))
}
}
// Preset view elements
struct PresetColor: ButtonStyle {
var fillColor: Color
var label: String
func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .center, spacing: 10) {
Circle()
.frame(width: 20, height: 20)
.fixedSize(horizontal: true, vertical: true)
.foregroundColor(fillColor)
.background(fillColor)
.clipShape(Circle())
.help(label)
.overlay(
RoundedRectangle(cornerRadius: 25)
.strokeBorder(Color("mode").opacity(0.8), lineWidth: 1))
// Text(label)
}
.onHover { inside in inside ? NSCursor.pointingHand.push() : NSCursor.pop() }
}
}
-131
View File
@@ -1,131 +0,0 @@
//
// ThemeManager.swift
// Pearcleaner
//
// Created by Alin Lupascu on 4/12/24.
//
import Foundation
import SwiftUI
import AppKit
class ThemeSettings: ObservableObject {
static let shared = ThemeSettings()
private let userDefaults = UserDefaults.standard
private let colorKey = "themeColor"
@Published var themeColor: Color {
didSet {
saveThemeColor()
}
}
// light - Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
// dark - Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1)
// pearcleaner - Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
init() {
themeColor = userDefaults.color(forKey: colorKey) ?? .clear
}
func setupInitialColor(forcedDarkMode: Bool? = nil) {
// Determine dark mode either from forced setting or system setting
let darkMode = forcedDarkMode ?? isDarkMode()
// Set the theme color based on the dark mode status
themeColor = darkMode ? Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1) :
Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
saveThemeColor()
}
private func loadThemeColor() {
if let components = UserDefaults.standard.array(forKey: colorKey) as? [CGFloat], components.count == 4 {
themeColor = Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
}
}
func saveThemeColor() {
let nsColor = NSColor(themeColor)
if let components = nsColor.cgColor.components {
UserDefaults.standard.set(components, forKey: colorKey)
}
}
func resetToDefault(dark: Bool = true) {
themeColor = dark ? Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
// themeColor = dark ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
saveThemeColor()
}
func setPreset(preset: String, colorScheme: DisplayMode) {
themeColor = getColorForPreset(preset: preset, colorScheme: colorScheme)
saveThemeColor()
}
func getColorForPreset(preset: String, colorScheme: DisplayMode) -> Color {
let pearcleaner = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.499549, green: 0.545169, blue: 0.682028, opacity: 1) : Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
let solarized = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.554372, green: 0.6557, blue: 0.734336, opacity: 1) : Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
let dracula = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.567094, green: 0.562125, blue: 0.81285, opacity: 1) : Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
let macOS = colorScheme.colorScheme == .light ? Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1) : Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1)
switch preset {
case "pearcleaner":
return pearcleaner
case "solarized":
return solarized
case "dracula":
return dracula
case "macOS":
return macOS
default:
return themeColor
}
}
}
extension Color {
/// Darkens the color by a percentage.
func darker(by percentage: CGFloat = 30.0) -> Color {
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: max(hsb.brightness - percentage/100, 0), opacity: hsb.alpha)
}
/// Lightens the color by a percentage.
func lighter(by percentage: CGFloat = 30.0) -> Color {
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: min(hsb.brightness + percentage / 100, 1.0), opacity: hsb.alpha)
}
}
extension Color {
/// Checks if the current themeColor is of a light or dark variant to set the appropriate displayMode on launch
func isLight() -> Bool? {
guard let components = NSColor(self).cgColor.components, components.count >= 3 else {
return nil
}
// Formula to calculate perceived luminance
let brightness = 0.299 * components[0] + 0.587 * components[1] + 0.114 * components[2]
return brightness > 0.5
}
}
extension UserDefaults {
func color(forKey key: String) -> Color? {
guard let components = array(forKey: key) as? [CGFloat], components.count == 4 else {
return nil
}
return Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
}
func setColor(_ color: Color, forKey key: String) {
let nsColor = NSColor(color)
if let components = nsColor.cgColor.components, components.count == 4 {
set(components, forKey: key)
} else {
removeObject(forKey: key)
}
}
}
-292
View File
@@ -1,292 +0,0 @@
//
// Updater.swift
// Pearcleaner
//
// Created by Alin Lupascu on 5/3/24.
//
import Foundation
import SwiftUI
struct Release: Codable {
let id: Int
let tag_name: String
let body: String
let assets: [Asset]
}
struct Asset: Codable {
let name: String
let url: String
let browser_download_url: String
}
extension Release {
var modifiedBody: String {
return body.replacingOccurrences(of: "- [x]", with: ">").replacingOccurrences(of: "###", with: "")
}
}
// --- Updater functionality
func loadGithubReleases(appState: AppState, manual: Bool = false, releaseOnly: Bool = false) {
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/releases")!
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode([Release].self, from: data) {
DispatchQueue.main.async {
let lastFewReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases
appState.releases = lastFewReleases
if !releaseOnly {
checkForUpdate(appState: appState, manual: manual)
}
}
return
}
}
printOS("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
func checkForUpdate(appState: AppState, manual: Bool) {
guard let latestRelease = appState.releases.first else { return }
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
if latestRelease.tag_name > currentVersion {
updateOnMain {
appState.updateAvailable = true
}
} else {
if manual {
NewWin.show(appState: appState, width: 500, height: 300, newWin: .no_update)
}
}
}
func downloadUpdate(appState: AppState) {
updateOnMain {
appState.progressBar.0 = "UPDATER: Getting update link"
appState.progressBar.1 = 0.1
}
let fileManager = FileManager.default
guard let latestRelease = appState.releases.first else { return }
guard let asset = latestRelease.assets.first else { return }
guard let url = URL(string: asset.url) else { return }
var request = URLRequest(url: url)
request.setValue("application/octet-stream", forHTTPHeaderField: "Accept")
let downloadTask = URLSession.shared.downloadTask(with: request) { localURL, urlResponse, error in
updateOnMain {
appState.progressBar.0 = "UPDATER: Starting download of update file"
appState.progressBar.1 = 0.2
}
guard let localURL = localURL else { return }
let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(asset.name)")
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try? fileManager.removeItem(at: destinationURL)
}
updateOnMain {
appState.progressBar.0 = "UPDATER: File downloaded to temp directory"
appState.progressBar.1 = 0.3
}
try fileManager.moveItem(at: localURL, to: destinationURL)
updateOnMain {
appState.progressBar.0 = "UPDATER: File renamed using asset name"
appState.progressBar.1 = 0.4
}
UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState)
} catch {
printOS("Error moving downloaded file: \(error.localizedDescription)")
}
}
downloadTask.resume()
}
func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) {
let appDirectory = Bundle.main.bundleURL.deletingLastPathComponent().path
let appBundle = Bundle.main.bundleURL.path
let fileManager = FileManager.default
do {
updateOnMain {
appState.progressBar.0 = "UPDATER: Removing currently installed application bundle"
appState.progressBar.1 = 0.5
}
// Remove the old version of your app
try fileManager.removeItem(atPath: appBundle)
updateOnMain {
appState.progressBar.0 = "UPDATER: Unziping file to original install location"
appState.progressBar.1 = 0.6
}
// Unzip the downloaded update file to your app's bundle path
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
process.arguments = ["-xk", fileURL, appDirectory]
process.standardOutput = FileHandle.nullDevice
process.standardError = FileHandle.nullDevice
try process.run()
process.waitUntilExit()
updateOnMain {
appState.progressBar.0 = "UPDATER: Removing file from temp directory"
appState.progressBar.1 = 0.8
}
// After unzipping, remove the update file
try fileManager.removeItem(atPath: fileURL)
updateOnMain {
appState.progressBar.0 = "UPDATER: Completed, please restart!"
appState.progressBar.1 = 1.0
appState.updateAvailable = false
}
} catch {
printOS("Error updating the app: \(error)")
}
}
// --- Updater check frequency
enum UpdateFrequency: String, CaseIterable, Identifiable {
case none = "Never"
case daily = "Daily"
case weekly = "Weekly"
case monthly = "Monthly"
var id: String { self.rawValue }
var interval: TimeInterval? {
switch self {
case .none:
return nil
case .daily:
return 86400 // 1 day in seconds
case .weekly:
return 604800 // 7 days in seconds
case .monthly:
return 2592000 // 30 days in seconds
}
}
func updateNextUpdateDate() {
guard let updateInterval = self.interval else { return }
let newUpdateDate = Calendar.current.startOfDay(for: Date().addingTimeInterval(updateInterval))
UserDefaults.standard.set(newUpdateDate.timeIntervalSinceReferenceDate, forKey: "settings.updater.nextUpdateDate")
}
}
func checkAndUpdateIfNeeded(appState: AppState) {
@AppStorage("settings.updater.updateFrequency") var updateFrequency: UpdateFrequency = .daily
@AppStorage("settings.updater.nextUpdateDate") var nextUpdateDate = Date.now.timeIntervalSinceReferenceDate
guard let updateInterval = updateFrequency.interval else {
printOS("Updater: no update frequency set, skipping check")
return
}
let now = Date()
let nextUpdateDateLocal = Date(timeIntervalSinceReferenceDate: nextUpdateDate)
if !isSameDay(date1: nextUpdateDateLocal, date2: now) {
printOS("Updater: next update date is in the future, skipping (\(nextUpdateDateLocal))")
return
}
updateApp(appState: appState)
setNextUpdateDate(interval: updateInterval)
}
func updateApp(appState: AppState) {
// Perform your update logic here
printOS("Updater: checking for update")
loadGithubReleases(appState: appState)
}
func setNextUpdateDate(interval: TimeInterval) {
let newUpdateDate = Calendar.current.startOfDay(for: Date().addingTimeInterval(interval))
UserDefaults.standard.set(newUpdateDate.timeIntervalSinceReferenceDate, forKey: "settings.updater.nextUpdateDate")
}
func isSameDay(date1: Date, date2: Date) -> Bool {
return Calendar.current.isDate(date1, inSameDayAs: date2)
}
// --- Updater Badge View
struct UpdateNotificationView: View {
let appState: AppState
@State private var hovered: Bool = false
var body: some View {
HStack {
Text("Update Available!")
.font(.callout)
.opacity(0.5)
.padding(.leading, 7)
Spacer()
HStack(alignment: .center) {
Image(systemName: !hovered ? "square.and.arrow.down" : "square.and.arrow.down.fill")
.resizable()
.scaledToFit()
.frame(width: 14, height: 14)
.animation(.easeInOut(duration: 0.2), value: hovered)
.foregroundStyle(.white)
Text("v\(appState.releases.first?.tag_name ?? "Update")")
.foregroundStyle(.white)
}
.padding(3)
.onHover { hovering in
withAnimation() {
hovered = hovering
}
}
.onTapGesture {
NewWin.show(appState: appState, width: 500, height: 440, newWin: .update)
}
.help("Download latest update")
.padding(.horizontal, 5)
.padding(.vertical, 4)
.background(Color("pear"))
.clipShape(RoundedRectangle(cornerRadius: 6))
}
.frame(height: 30)
.padding(5)
.background(Color("mode").opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 6))
.padding(.horizontal)
.padding(.bottom)
}
}
+318 -319
View File
@@ -7,30 +7,29 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import OSLog import AlinFoundation
import CoreImage
import AppKit import AppKit
// Make updates on main thread // Make updates on main thread
func updateOnMain(after delay: Double? = nil, _ updates: @escaping () -> Void) { //func updateOnMain(after delay: Double? = nil, _ updates: @escaping () -> Void) {
if let delay = delay { // if let delay = delay {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { // DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
updates() // updates()
} // }
} else { // } else {
DispatchQueue.main.async { // DispatchQueue.main.async {
updates() // updates()
} // }
} // }
} //}
//
//
// Execute functions on background thread //// Execute functions on background thread
func updateOnBackground(_ updates: @escaping () -> Void) { //func updateOnBackground(_ updates: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async { // DispatchQueue.global(qos: .userInitiated).async {
updates() // updates()
} // }
} //}
// Reload apps list // Reload apps list
func reloadAppsList(appState: AppState, fsm: FolderSettingsManager) { func reloadAppsList(appState: AppState, fsm: FolderSettingsManager) {
@@ -57,57 +56,57 @@ func resizeWindowAuto(windowSettings: WindowSettings, title: String) {
// Check app directory based on user permission // Check app directory based on user permission
func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) { //func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) {
isCurrentUserAdmin { isAdmin in // isCurrentUserAdmin { isAdmin in
let bundlePath = Bundle.main.bundlePath as NSString // let bundlePath = Bundle.main.bundlePath as NSString
let applicationsDir = "/Applications" // let applicationsDir = "/Applications"
let userApplicationsDir = "\(home)/Applications" // let userApplicationsDir = "\(home)/Applications"
//
var isInCorrectDirectory = false // var isInCorrectDirectory = false
//
if isAdmin { // if isAdmin {
// Admins can have the app in either /Applications or ~/Applications // // Admins can have the app in either /Applications or ~/Applications
isInCorrectDirectory = bundlePath.deletingLastPathComponent == applicationsDir || // isInCorrectDirectory = bundlePath.deletingLastPathComponent == applicationsDir ||
bundlePath.deletingLastPathComponent == userApplicationsDir // bundlePath.deletingLastPathComponent == userApplicationsDir
} else { // } else {
// Standard users should only have the app in ~/Applications // // Standard users should only have the app in ~/Applications
isInCorrectDirectory = bundlePath.deletingLastPathComponent == userApplicationsDir // isInCorrectDirectory = bundlePath.deletingLastPathComponent == userApplicationsDir
} // }
//
// Return both conditions: if the app is in the correct directory and if the user is an admin // // Return both conditions: if the app is in the correct directory and if the user is an admin
completion((isInCorrectDirectory, isAdmin)) // completion((isInCorrectDirectory, isAdmin))
} // }
} //}
//
//
// Check if user is admin or standard user //// Check if user is admin or standard user
func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) { //func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
let process = Process() // let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/zsh") // Using zsh, macOS default shell // process.executableURL = URL(fileURLWithPath: "/bin/zsh") // Using zsh, macOS default shell
process.arguments = ["-c", "groups $(whoami) | grep -q ' admin '"] // process.arguments = ["-c", "groups $(whoami) | grep -q ' admin '"]
//
process.terminationHandler = { process in // process.terminationHandler = { process in
// On macOS, a process's exit status of 0 indicates success (admin group found in this context) // // On macOS, a process's exit status of 0 indicates success (admin group found in this context)
completion(process.terminationStatus == 0) // completion(process.terminationStatus == 0)
} // }
//
do { // do {
try process.run() // try process.run()
} catch { // } catch {
print("Failed to execute command: \(error)") // print("Failed to execute command: \(error)")
completion(false) // completion(false)
} // }
} //}
// Check if appearance is dark mode // Check if appearance is dark mode
func isDarkMode() -> Bool { //func isDarkMode() -> Bool {
return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua // return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
} //}
//
// Set app color mode //// Set app color mode
func setAppearance(mode: NSAppearance.Name) { //func setAppearance(mode: NSAppearance.Name) {
NSApp.appearance = NSAppearance(named: mode) // NSApp.appearance = NSAppearance(named: mode)
} //}
// Check if Pearcleaner has any windows open // Check if Pearcleaner has any windows open
func hasWindowOpen() -> Bool { func hasWindowOpen() -> Bool {
@@ -118,13 +117,13 @@ func hasWindowOpen() -> Bool {
} }
// Find and hide/show main app window when using menubar item // Find and hide/show main app window when using menubar item
func findAndHideWindows(named titles: [String]) { //func findAndHideWindows(named titles: [String]) {
for title in titles { // for title in titles {
if let window = NSApp.windows.first(where: { $0.title == title }) { // if let window = NSApp.windows.first(where: { $0.title == title }) {
window.close() // window.close()
} // }
} // }
} //}
func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSettings) { func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSettings) {
windowSettings.registerDefaultWindowSettings() { windowSettings.registerDefaultWindowSettings() {
@@ -139,20 +138,20 @@ func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSetting
} }
func findAndShowWindows(named titles: [String]) { //func findAndShowWindows(named titles: [String]) {
for title in titles { // for title in titles {
if let window = NSApp.windows.first(where: { $0.title == title }) { // if let window = NSApp.windows.first(where: { $0.title == title }) {
window.makeKeyAndOrderFront(nil) // window.makeKeyAndOrderFront(nil)
} // }
} // }
} //}
// Copy to clipboard // Copy to clipboard
func copyToClipboard(text: String) { //func copyToClipboard(text: String) {
let pasteboard = NSPasteboard.general // let pasteboard = NSPasteboard.general
pasteboard.clearContents() // pasteboard.clearContents()
pasteboard.setString(text, forType: .string) // pasteboard.setString(text, forType: .string)
} //}
@@ -224,15 +223,15 @@ func saveURLsToFile(urls: Set<URL>, appState: AppState) {
// Check if symlink // Check if symlink
func isSymlink(atPath path: URL) -> Bool { //func isSymlink(atPath path: URL) -> Bool {
do { // do {
let _ = try path.checkResourceIsReachable() // let _ = try path.checkResourceIsReachable()
let resourceValues = try path.resourceValues(forKeys: [.isSymbolicLinkKey]) // let resourceValues = try path.resourceValues(forKeys: [.isSymbolicLinkKey])
return resourceValues.isSymbolicLink == true // return resourceValues.isSymbolicLink == true
} catch { // } catch {
return false // return false
} // }
} //}
// Open trash folder // Open trash folder
@@ -297,62 +296,62 @@ func checkAppBundleArchitecture(at appBundlePath: String) -> Arch {
// Convert icon to png so colors render correctly // Convert icon to png so colors render correctly
func convertICNSToPNG(icon: NSImage, size: NSSize) -> NSImage? { //func convertICNSToPNG(icon: NSImage, size: NSSize) -> NSImage? {
// Resize the icon to the specified size // // Resize the icon to the specified size
let resizedIcon = NSImage(size: size) // let resizedIcon = NSImage(size: size)
resizedIcon.lockFocus() // resizedIcon.lockFocus()
icon.draw(in: NSRect(x: 0, y: 0, width: size.width, height: size.height)) // icon.draw(in: NSRect(x: 0, y: 0, width: size.width, height: size.height))
resizedIcon.unlockFocus() // resizedIcon.unlockFocus()
//
// Convert the resized icon to PNG format // // Convert the resized icon to PNG format
if let resizedImageData = resizedIcon.tiffRepresentation, // if let resizedImageData = resizedIcon.tiffRepresentation,
let resizedBitmap = NSBitmapImageRep(data: resizedImageData), // let resizedBitmap = NSBitmapImageRep(data: resizedImageData),
let pngData = resizedBitmap.representation(using: .png, properties: [:]) { // let pngData = resizedBitmap.representation(using: .png, properties: [:]) {
return NSImage(data: pngData) // return NSImage(data: pngData)
} // }
//
return nil // return nil
} //}
// Get icon for files and folders // Get icon for files and folders
func getIconForFileOrFolder(atPath path: URL) -> Image? { //func getIconForFileOrFolder(atPath path: URL) -> Image? {
return Image(nsImage: NSWorkspace.shared.icon(forFile: path.path)) // return Image(nsImage: NSWorkspace.shared.icon(forFile: path.path))
} //}
//
func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? { //func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? {
return NSWorkspace.shared.icon(forFile: path.path) // return NSWorkspace.shared.icon(forFile: path.path)
} //}
// Get average color from image // Get average color from image
extension NSImage { //extension NSImage {
var averageColor: NSColor? { // var averageColor: NSColor? {
guard let tiffData = self.tiffRepresentation, let bitmapImage = NSBitmapImageRep(data: tiffData), let inputImage = CIImage(bitmapImageRep: bitmapImage) else { return nil } // guard let tiffData = self.tiffRepresentation, let bitmapImage = NSBitmapImageRep(data: tiffData), let inputImage = CIImage(bitmapImageRep: bitmapImage) else { return nil }
//
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height) // let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
//
guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil } // guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
guard let outputImage = filter.outputImage else { return nil } // guard let outputImage = filter.outputImage else { return nil }
//
var bitmap = [UInt8](repeating: 0, count: 4) // var bitmap = [UInt8](repeating: 0, count: 4)
let context = CIContext(options: [.workingColorSpace: NSNull()]) // let context = CIContext(options: [.workingColorSpace: NSNull()])
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: CIFormat.RGBA8, colorSpace: nil) // context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: CIFormat.RGBA8, colorSpace: nil)
//
return NSColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255) // return NSColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
} // }
} //}
// Relaunch app // Relaunch app
func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never { //func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never {
let task = Process() // let task = Process()
task.launchPath = "/bin/sh" // task.launchPath = "/bin/sh"
task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""] // task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
task.launch() // task.launch()
//
NSApp.terminate(nil) // NSApp.terminate(nil)
exit(0) // exit(0)
} //}
// Check if app is running before deleting app files // Check if app is running before deleting app files
func killApp(appId: String, completion: @escaping () -> Void = {}) { func killApp(appId: String, completion: @escaping () -> Void = {}) {
@@ -416,7 +415,7 @@ func folderImages(for path: String) -> AnyView? {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 13) .frame(width: 13)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
.help("Container") .help("Container")
) )
} else if path.contains("/Library/Application Scripts/") { } else if path.contains("/Library/Application Scripts/") {
@@ -425,7 +424,7 @@ func folderImages(for path: String) -> AnyView? {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 13) .frame(width: 13)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
.help("Application Script") .help("Application Script")
) )
} else if path.contains(".plist") { } else if path.contains(".plist") {
@@ -434,7 +433,7 @@ func folderImages(for path: String) -> AnyView? {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 13) .frame(width: 13)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
.help("Plist File") .help("Plist File")
) )
} }
@@ -461,22 +460,22 @@ func isNested(path: URL) -> Bool {
} }
// Check if macOS is 14.0 or higher // Check if macOS is 14.0 or higher
func isMacOS14OrHigher() -> Bool { //func isMacOS14OrHigher() -> Bool {
if #available(macOS 14, *) { // if #available(macOS 14, *) {
return true // return true
} else { // } else {
return false // return false
} // }
} //}
// --- Extend Int to convert hours to seconds --- // --- Extend Int to convert hours to seconds ---
extension Int { //extension Int {
var daysToSeconds: Double { // var daysToSeconds: Double {
return Double(self) * 24 * 60 * 60 // return Double(self) * 24 * 60 * 60
} // }
} //}
// --- Extend String to remove periods, spaces and lowercase the string // --- Extend String to remove periods, spaces and lowercase the string
@@ -495,15 +494,15 @@ extension String {
} }
// --- Capitalize first letter of string only // --- Capitalize first letter of string only
extension String { //extension String {
func capitalizingFirstLetter() -> String { // func capitalizingFirstLetter() -> String {
return prefix(1).capitalized + dropFirst() // return prefix(1).capitalized + dropFirst()
} // }
//
mutating func capitalizeFirstLetter() { // mutating func capitalizeFirstLetter() {
self = self.capitalizingFirstLetter() // self = self.capitalizingFirstLetter()
} // }
} //}
// --- Returns comma separated string as array of strings // --- Returns comma separated string as array of strings
extension String { extension String {
@@ -517,18 +516,18 @@ extension String {
// --- Overload the greater than operator ">" to do a semantic check on the string versions // --- Overload the greater than operator ">" to do a semantic check on the string versions
extension String { //extension String {
func versionStringToTuple() -> (Int, Int, Int) { // func versionStringToTuple() -> (Int, Int, Int) {
let components = self.split(separator: ".").compactMap { Int($0) } // let components = self.split(separator: ".").compactMap { Int($0) }
return (components[0], components[1], components[2]) // return (components[0], components[1], components[2])
} // }
//
static func > (lhs: String, rhs: String) -> Bool { // static func > (lhs: String, rhs: String) -> Bool {
let lhsVersion = lhs.versionStringToTuple() // let lhsVersion = lhs.versionStringToTuple()
let rhsVersion = rhs.versionStringToTuple() // let rhsVersion = rhs.versionStringToTuple()
return lhsVersion > rhsVersion // return lhsVersion > rhsVersion
} // }
} //}
// --- Trash Relationship --- // --- Trash Relationship ---
@@ -541,103 +540,103 @@ extension String {
//} //}
// --- Extend print command to also output to the Console --- // --- Extend print command to also output to the Console ---
func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") { //func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
let message = items.map { "\($0)" }.joined(separator: separator) // let message = items.map { "\($0)" }.joined(separator: separator)
let log = OSLog(subsystem: "pearcleaner", category: "Application") // let log = OSLog(subsystem: "pearcleaner", category: "Application")
os_log("%@", log: log, type: .default, message) // os_log("%@", log: log, type: .default, message)
} //}
// Get size of files // Get size of files
func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) { //func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) {
let fileManager = FileManager.default // let fileManager = FileManager.default
var totalAllocatedSize: Int64 = 0 // var totalAllocatedSize: Int64 = 0
var totalFileSize: Int64 = 0 // var totalFileSize: Int64 = 0
//
for url in paths { // for url in paths {
var isDirectory: ObjCBool = false // var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) { // if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey] // let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey]
if isDirectory.boolValue { // if isDirectory.boolValue {
// It's a directory, recurse into it // // It's a directory, recurse into it
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) { // if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) {
for case let fileURL as URL in enumerator { // for case let fileURL as URL in enumerator {
do { // do {
let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys)) // let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys))
if let allocatedSize = fileAttributes.totalFileAllocatedSize { // if let allocatedSize = fileAttributes.totalFileAllocatedSize {
totalAllocatedSize += Int64(allocatedSize) // totalAllocatedSize += Int64(allocatedSize)
} // }
if let fileSize = fileAttributes.fileSize { // if let fileSize = fileAttributes.fileSize {
totalFileSize += Int64(fileSize) // totalFileSize += Int64(fileSize)
} // }
} catch { // } catch {
print("Error getting file attributes for \(fileURL): \(error)") // print("Error getting file attributes for \(fileURL): \(error)")
} // }
} // }
} // }
} else { // } else {
// It's a file // // It's a file
do { // do {
let fileAttributes = try url.resourceValues(forKeys: Set(keys)) // let fileAttributes = try url.resourceValues(forKeys: Set(keys))
if let allocatedSize = fileAttributes.totalFileAllocatedSize { // if let allocatedSize = fileAttributes.totalFileAllocatedSize {
totalAllocatedSize += Int64(allocatedSize) // totalAllocatedSize += Int64(allocatedSize)
} // }
if let fileSize = fileAttributes.fileSize { // if let fileSize = fileAttributes.fileSize {
totalFileSize += Int64(fileSize) // totalFileSize += Int64(fileSize)
} // }
} catch { // } catch {
print("Error getting file attributes for \(url): \(error)") // print("Error getting file attributes for \(url): \(error)")
} // }
} // }
} // }
} // }
//
return (real: totalAllocatedSize, logical: totalFileSize) // return (real: totalAllocatedSize, logical: totalFileSize)
} //}
//
//
//
func totalSizeOnDisk(for path: URL) -> (real: Int64, logical: Int64) { //func totalSizeOnDisk(for path: URL) -> (real: Int64, logical: Int64) {
return totalSizeOnDisk(for: [path]) // return totalSizeOnDisk(for: [path])
} //}
//
// ByteFormatter //// ByteFormatter
func formatByte(size: Int64) -> (human: String, byte: String) { //func formatByte(size: Int64) -> (human: String, byte: String) {
let byteCountFormatter = ByteCountFormatter() // let byteCountFormatter = ByteCountFormatter()
byteCountFormatter.countStyle = .file // byteCountFormatter.countStyle = .file
byteCountFormatter.allowedUnits = [.useAll] // byteCountFormatter.allowedUnits = [.useAll]
let human = byteCountFormatter.string(fromByteCount: size) // let human = byteCountFormatter.string(fromByteCount: size)
//
let numberformatter = NumberFormatter() // let numberformatter = NumberFormatter()
numberformatter.numberStyle = .decimal // numberformatter.numberStyle = .decimal
let formattedNumber = numberformatter.string(from: NSNumber(value: size)) ?? "\(size)" // let formattedNumber = numberformatter.string(from: NSNumber(value: size)) ?? "\(size)"
let byte = "\(formattedNumber)" // let byte = "\(formattedNumber)"
//
return (human: human, byte: byte) // return (human: human, byte: byte)
//
} //}
// Only process supported files // Only process supported files
func isSupportedFileType(at path: String) -> Bool { //func isSupportedFileType(at path: String) -> Bool {
let fileManager = FileManager.default // let fileManager = FileManager.default
do { // do {
let attributes = try fileManager.attributesOfItem(atPath: path) // let attributes = try fileManager.attributesOfItem(atPath: path)
if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType { // if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType {
switch fileType { // switch fileType {
case .typeRegular, .typeDirectory, .typeSymbolicLink: // case .typeRegular, .typeDirectory, .typeSymbolicLink:
// The file is a regular file, directory, or symbolic link // // The file is a regular file, directory, or symbolic link
return true // return true
default: // default:
// The file is a socket, pipe, or another type not supported // // The file is a socket, pipe, or another type not supported
return false // return false
} // }
} // }
} catch { // } catch {
printOS("Error getting file attributes: \(error)") // printOS("Error getting file attributes: \(error)")
} // }
return false // return false
} //}
@@ -671,43 +670,43 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
// --- Create Application Support folder if it doesn't exist --- // --- Create Application Support folder if it doesn't exist ---
func ensureApplicationSupportFolderExists(appState: AppState) { //func ensureApplicationSupportFolderExists(appState: AppState) {
let fileManager = FileManager.default // let fileManager = FileManager.default
let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("com.alienator88.Pearcleaner") // let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("com.alienator88.Pearcleaner")
//
// Check to make sure Application Support/Pearcleaner folder exists // // Check to make sure Application Support/Pearcleaner folder exists
if !fileManager.fileExists(atPath: supportURL.path) { // if !fileManager.fileExists(atPath: supportURL.path) {
try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true) // try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
printOS("Created Application Support/com.alienator88.Pearcleaner folder") // printOS("Created Application Support/com.alienator88.Pearcleaner folder")
} // }
} //}
// --- Write Log to File for troubleshooting --- // --- Write Log to File for troubleshooting ---
func writeLog(string: String) { //func writeLog(string: String) {
let fileManager = FileManager.default // let fileManager = FileManager.default
let home = fileManager.homeDirectoryForCurrentUser.path // let home = fileManager.homeDirectoryForCurrentUser.path
let logFilePath = "\(home)/Downloads/log.txt" // let logFilePath = "\(home)/Downloads/log.txt"
//
// Check if the log file exists, and create it if it doesn't // // Check if the log file exists, and create it if it doesn't
if !fileManager.fileExists(atPath: logFilePath) { // if !fileManager.fileExists(atPath: logFilePath) {
if !fileManager.createFile(atPath: logFilePath, contents: nil, attributes: nil) { // if !fileManager.createFile(atPath: logFilePath, contents: nil, attributes: nil) {
printOS("Failed to create the log file.") // printOS("Failed to create the log file.")
return // return
} // }
} // }
//
do { // do {
if let fileHandle = FileHandle(forWritingAtPath: logFilePath) { // if let fileHandle = FileHandle(forWritingAtPath: logFilePath) {
let ns = "\(string)\n" // let ns = "\(string)\n"
fileHandle.seekToEndOfFile() // fileHandle.seekToEndOfFile()
fileHandle.write(ns.data(using: .utf8)!) // fileHandle.write(ns.data(using: .utf8)!)
fileHandle.closeFile() // fileHandle.closeFile()
} else { // } else {
printOS("Error opening file for appending") // printOS("Error opening file for appending")
} // }
} // }
} //}
// --- Load Plist file with launchctl --- // --- Load Plist file with launchctl ---
@@ -752,20 +751,20 @@ func sendStopNotificationFW() {
} }
func getCurrentTimestamp() -> String { //func getCurrentTimestamp() -> String {
let dateFormatter = DateFormatter() // let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" // dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return dateFormatter.string(from: Date()) // return dateFormatter.string(from: Date())
} //}
func formattedDate(_ date: Date?) -> String { //func formattedDate(_ date: Date?) -> String {
guard let date = date else { return "N/A" } // guard let date = date else { return "N/A" }
let formatter = DateFormatter() // let formatter = DateFormatter()
formatter.dateStyle = .short // formatter.dateStyle = .short
formatter.timeStyle = .none // formatter.timeStyle = .none
return formatter.string(from: date) // return formatter.string(from: date)
} //}
+31 -87
View File
@@ -7,6 +7,7 @@
import SwiftUI import SwiftUI
import AppKit import AppKit
import AlinFoundation
@main @main
struct PearcleanerApp: App { struct PearcleanerApp: App {
@@ -14,10 +15,11 @@ struct PearcleanerApp: App {
@StateObject var appState = AppState() @StateObject var appState = AppState()
@StateObject var locations = Locations() @StateObject var locations = Locations()
@StateObject var fsm = FolderSettingsManager() @StateObject var fsm = FolderSettingsManager()
@StateObject private var updater = Updater(owner: "alienator88", repo: "Pearcleaner")
@StateObject private var themeManager = ThemeManager.shared
@StateObject private var permissionManager = PermissionManager.shared
@State private var windowSettings = WindowSettings() @State private var windowSettings = WindowSettings()
@AppStorage("settings.updater.updateFrequency") private var updateFrequency: UpdateFrequency = .daily
@AppStorage("settings.permissions.hasLaunched") private var hasLaunched: Bool = false @AppStorage("settings.permissions.hasLaunched") private var hasLaunched: Bool = false
@AppStorage("displayMode") var displayMode: DisplayMode = .system
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.general.miniview") private var miniView: Bool = true @AppStorage("settings.general.miniview") private var miniView: Bool = true
@AppStorage("settings.general.features") private var features: String = "" @AppStorage("settings.general.features") private var features: String = ""
@@ -42,8 +44,10 @@ struct PearcleanerApp: App {
.environmentObject(appState) .environmentObject(appState)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(fsm) .environmentObject(fsm)
.environmentObject(ThemeSettings.shared) .environmentObject(themeManager)
.preferredColorScheme(displayMode.colorScheme) .environmentObject(updater)
.environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
.handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*")) .handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*"))
.onOpenURL(perform: { url in .onOpenURL(perform: { url in
let deeplinkManager = DeeplinkManager(showPopover: $showPopover) let deeplinkManager = DeeplinkManager(showPopover: $showPopover)
@@ -76,6 +80,11 @@ struct PearcleanerApp: App {
secondaryButton: .cancel() secondaryButton: .cancel()
) )
} }
.sheet(isPresented: $updater.showSheet, content: {
/// This will show the update sheet based on the frequency check function only
updater.getUpdateView()
.environmentObject(themeManager)
})
.onAppear { .onAppear {
if miniView { if miniView {
@@ -98,8 +107,9 @@ struct PearcleanerApp: App {
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm) .environmentObject(fsm)
.environmentObject(ThemeSettings.shared) .environmentObject(themeManager)
.preferredColorScheme(displayMode.colorScheme) .environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}, icon: selectedMenubarIcon) }, icon: selectedMenubarIcon)
} }
@@ -107,29 +117,15 @@ struct PearcleanerApp: App {
#if !DEBUG #if !DEBUG
Task { Task {
/// This will check for updates on load based on the update frequency
updater.checkAndUpdateIfNeeded()
/// Get new features
getFeatures(appState: appState, features: $features)
// Make sure App Support folder exists in the future if needed for storage // Make sure App Support folder exists in the future if needed for storage
//MARK: This is not needed any longer as the update file is stored in /tmp directory. Use in the future for any local db functions the app might need //MARK: This is not needed any longer as the update file is stored in /tmp directory. Use in the future for any local db functions the app might need
// ensureApplicationSupportFolderExists(appState: appState) // ensureApplicationSupportFolderExists(appState: appState)
// Check for updates after app launch
checkAllPermissions(appState: appState) { results in
appState.permissionResults = results
if results.allPermissionsGranted {
// Get GH releases
loadGithubReleases(appState: appState, releaseOnly: true)
if updateFrequency != .none {
// Update checker
checkAndUpdateIfNeeded(appState: appState)
}
// Get new features
getFeatures(appState: appState, features: $features)
}
}
} }
@@ -140,7 +136,7 @@ struct PearcleanerApp: App {
.windowStyle(.hiddenTitleBar) .windowStyle(.hiddenTitleBar)
.windowResizability(.contentMinSize) .windowResizability(.contentMinSize)
.commands { .commands {
AppCommands(appState: appState, locations: locations, fsm: fsm) AppCommands(appState: appState, locations: locations, fsm: fsm, updater: updater, themeManager: themeManager)
// CommandGroup(replacing: .newItem, addition: { }) // CommandGroup(replacing: .newItem, addition: { })
} }
@@ -153,9 +149,11 @@ struct PearcleanerApp: App {
.environmentObject(appState) .environmentObject(appState)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(fsm) .environmentObject(fsm)
.environmentObject(ThemeSettings.shared) .environmentObject(themeManager)
.environmentObject(updater)
.environmentObject(permissionManager)
.toolbarBackground(.clear) .toolbarBackground(.clear)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(themeManager.displayMode.colorScheme)
} }
} }
} }
@@ -164,8 +162,8 @@ struct PearcleanerApp: App {
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
var observer: NSObjectProtocol?
var windowSettings = WindowSettings() var windowSettings = WindowSettings()
var themeManager = ThemeManager.shared
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled") let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
@@ -178,72 +176,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
findAndSetWindowFrame(named: ["Pearcleaner"], windowSettings: windowSettings) findAndSetWindowFrame(named: ["Pearcleaner"], windowSettings: windowSettings)
self.appearanceCheck() themeManager.setupAppearance()
if menubarEnabled { if menubarEnabled {
findAndHideWindows(named: ["Pearcleaner"]) findAndHideWindows(named: ["Pearcleaner"])
NSApplication.shared.setActivationPolicy(.accessory) NSApplication.shared.setActivationPolicy(.accessory)
} }
// Start observing the appearance change
observer = DistributedNotificationCenter.default().addObserver(forName: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"), object: nil, queue: OperationQueue.main) { [weak self] _ in
let themeMode = UserDefaults.standard.string(forKey: "settings.general.selectedTheme")
let themesEnabled = UserDefaults.standard.bool(forKey: "settings.interface.themesEnabled")
if themeMode == "Auto" && !themesEnabled {
self?.appearanceCheck(reset: true)
}
}
} }
func appearanceCheck(reset: Bool = false) {
let themesEnabled = UserDefaults.standard.bool(forKey: "settings.interface.themesEnabled")
let themeMode = UserDefaults.standard.string(forKey: "settings.general.selectedTheme")
if themesEnabled {
// Custom theming is enabled; user's manual settings take priority.
} else {
switch themeMode {
case "Auto":
// Auto mode: adjust theme based on system appearance.
ThemeSettings.shared.setupInitialColor()
updateDisplayModeBasedOnThemeColor()
case "Light", "Dark":
// Specific mode set: ensure theme color matches the display mode.
ThemeSettings.shared.setupInitialColor(forcedDarkMode: themeMode == "Dark")
UserDefaults.standard.set(themeMode == "Dark" ? DisplayMode.dark.rawValue : DisplayMode.light.rawValue, forKey: "displayMode")
default:
// No specific handling needed or unrecognized mode; fall back to default behavior.
printOS("No specific theme mode set or unrecognized value.")
}
}
if reset && themeMode == "Auto" {
let dark = isDarkMode()
ThemeSettings.shared.resetToDefault(dark: dark)
}
}
func updateDisplayModeBasedOnThemeColor() {
let themeColor = ThemeSettings.shared.themeColor
if let isLightColor = themeColor.isLight() {
let shouldUseDarkMode = !isLightColor
UserDefaults.standard.set(shouldUseDarkMode ? DisplayMode.dark.rawValue : DisplayMode.light.rawValue, forKey: "displayMode")
} else {
// Default to system preference if brightness cannot be determined.
let dark = isDarkMode()
UserDefaults.standard.set(dark ? DisplayMode.dark.rawValue : DisplayMode.light.rawValue, forKey: "displayMode")
}
}
func applicationWillTerminate(_ notification: Notification) { func applicationWillTerminate(_ notification: Notification) {
// Stop observing the appearance change // Perform actions on app termination here
if let observer = observer {
DistributedNotificationCenter.default().removeObserver(observer)
}
} }
@@ -255,10 +201,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
for window in sender.windows { for window in sender.windows {
window.title = "Pearcleaner" window.title = "Pearcleaner"
window.makeKeyAndOrderFront(self) window.makeKeyAndOrderFront(self)
print(windowSettings.loadWindowSettings())
updateOnMain(after: 0.1, { updateOnMain(after: 0.1, {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner") resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
print(window.title)
}) })
} }
return true // Indicates you've handled the re-open return true // Indicates you've handled the re-open
@@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFE"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0x26",
"green" : "0x26",
"red" : "0x26"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
+93 -99
View File
@@ -6,12 +6,18 @@
// //
import SwiftUI import SwiftUI
import AlinFoundation
struct AboutSettingsTab: View { struct AboutSettingsTab: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@State private var disclose = false
@State private var discloseCredits = false
var body: some View { var body: some View {
let sponsors = Sponsor.sponsors
let credits = Credit.credits
VStack(alignment: .center) { VStack(alignment: .center) {
@@ -39,7 +45,7 @@ struct AboutSettingsTab: View {
Text("GitHub") Text("GitHub")
Text("Submit a bug or feature request via the repo") Text("Submit a bug or feature request via the repo")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
Spacer() Spacer()
@@ -53,133 +59,92 @@ struct AboutSettingsTab: View {
.padding(.leading) .padding(.leading)
.padding(.top, 5) .padding(.top, 5)
}
Divider() Divider()
.padding()
HStack{
Image(systemName: "dollarsign.circle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
DisclosureGroup(isExpanded: $disclose, content: {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(sponsors) { sponsor in
HStack() {
Text(sponsor.name)
Spacer()
Button(""){
NSWorkspace.shared.open(sponsor.url)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View", padding: 5))
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.frame(height: 100)
.padding(5)
}, label: {
Text("GitHub Sponsors")
.font(.title2)
})
} }
.padding(5)
.padding(.leading)
Divider()
HStack{
Image(systemName: "info.circle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
DisclosureGroup(isExpanded: $discloseCredits, content: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack() { ForEach(credits) { credit in
Text("Credits").font(.title2)
Spacer()
}
.padding(.leading)
.padding(.bottom)
HStack{ HStack{
Image(systemName: "paintbrush.pointed")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){ VStack(alignment: .leading){
Text("Microsoft Designer") Text(credit.name)
Text("Application icon resource") Text(credit.description)
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
Spacer() Spacer()
Button(""){ Button(""){
NSWorkspace.shared.open(URL(string: "https://designer.microsoft.com/image-creator")!) NSWorkspace.shared.open(credit.url)
} }
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View")) .buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
} }
}
}
.padding(5) .padding(5)
.padding(.leading) }, label: {
Text("Credits")
.font(.title2)
})
HStack{
Image(systemName: "applescript")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){
Text("Privacy Guides")
Text("Inspired by open-source appcleaner script from Sun Knudsen")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.lineLimit(2)
}
Spacer()
Button("") {
NSWorkspace.shared.open(URL(string: "https://sunknudsen.com/privacy-guides/how-to-clean-uninstall-macos-apps-using-appcleaner-open-source-alternative")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
} }
.padding(5) .padding(5)
.padding(.leading) .padding(.leading)
HStack{
Image(systemName: "trash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){
Text("AppCleaner")
Text("Inspired by AppCleaner from Freemacsoft")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
Spacer()
Button(""){
NSWorkspace.shared.open(URL(string: "https://freemacsoft.net/appcleaner/")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
}
.padding(5)
.padding(.leading)
HStack{
Image(systemName: "dollarsign.circle.fill")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){
Text("Apple Developer Account Sponsor")
Text("Many thanks to user DharsanB")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
Spacer()
Button(""){
NSWorkspace.shared.open(URL(string: "https://github.com/dharsanb")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
}
.padding(5)
.padding(.leading)
Spacer()
}
Spacer()
Text("Made with ❤️ by Alin Lupascu").font(.footnote).padding(.bottom) Text("Made with ❤️ by Alin Lupascu").font(.footnote).padding(.bottom)
} }
.padding(20) .padding(20)
.frame(width: 500, height: 600) .frame(width: 500)
} }
} }
@@ -207,3 +172,32 @@ extension Bundle {
} }
//MARK: Sponsors
struct Sponsor: Identifiable {
let id = UUID()
let name: String
let url: URL
static let sponsors: [Sponsor] = [
Sponsor(name: "DharsanB", url: URL(string: "https://github.com/dharsanb")!),
Sponsor(name: "MadMacMad", url: URL(string: "https://github.com/MadMacMad")!),
Sponsor(name: "Butterdawgs", url: URL(string: "https://github.com/butterdawgs")!),
Sponsor(name: "y-u-s-u-f", url: URL(string: "https://github.com/y-u-s-u-f")!)
]
}
//MARK: Credits
struct Credit: Identifiable {
let id = UUID()
// let icon: String
let name: String
let description: String
let url: URL
static let credits: [Credit] = [
Credit(name: "Microsoft Designer", description: "Application icon resource", url: URL(string: "https://designer.microsoft.com/image-creator")!),
Credit(name: "Privacy Guides", description: "Inspired by open-source appcleaner script from Sun Knudsen", url: URL(string: "https://sunknudsen.com/privacy-guides/how-to-clean-uninstall-macos-apps-using-appcleaner-open-source-alternative")!),
Credit(name: "AppCleaner", description: "Inspired by AppCleaner from Freemacsoft", url: URL(string: "https://freemacsoft.net/appcleaner/")!)
]
}
+9 -10
View File
@@ -15,12 +15,13 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AppKit import AppKit
import AlinFoundation
struct FolderSettingsTab: View { struct FolderSettingsTab: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager @EnvironmentObject var fsm: FolderSettingsManager
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
@State private var isHovered = false @State private var isHovered = false
var body: some View { var body: some View {
@@ -82,8 +83,9 @@ struct FolderSettingsTab: View {
} }
.scrollIndicators(.automatic) .scrollIndicators(.automatic)
.padding() .padding()
// .background(Color("mode").opacity(0.05)) .frame(height: 200)
.background(backgroundView(themeSettings: themeSettings, darker: true)) // .background(.primary.opacity(0.05))
.background(backgroundView(themeManager: themeManager, darker: true))
.clipShape(RoundedRectangle(cornerRadius: 10)) .clipShape(RoundedRectangle(cornerRadius: 10))
.onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in .onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in
providers.forEach { provider in providers.forEach { provider in
@@ -175,8 +177,9 @@ struct FolderSettingsTab: View {
} }
.scrollIndicators(.automatic) .scrollIndicators(.automatic)
.padding() .padding()
.background(backgroundView(themeSettings: themeSettings, darker: true)) .frame(height: 200)
// .background(Color("mode").opacity(0.05)) .background(backgroundView(themeManager: themeManager, darker: true))
// .background(.primary.opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 10)) .clipShape(RoundedRectangle(cornerRadius: 10))
.onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in .onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in
providers.forEach { provider in providers.forEach { provider in
@@ -200,15 +203,11 @@ struct FolderSettingsTab: View {
Spacer() Spacer()
} }
.padding(.top, 5) .padding(.top, 5)
Spacer()
} }
} }
.padding(20) .padding(20)
.frame(width: 500, height: 600) .frame(width: 500)//, height: 600)
} }
+121 -145
View File
@@ -7,13 +7,13 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
//import ServiceManagement
import FinderSync import FinderSync
import AlinFoundation
struct GeneralSettingsTab: View { struct GeneralSettingsTab: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
@State private var windowSettings = WindowSettings() @State private var windowSettings = WindowSettings()
@AppStorage("settings.general.glass") private var glass: Bool = true @AppStorage("settings.general.glass") private var glass: Bool = true
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@@ -52,11 +52,11 @@ struct GeneralSettingsTab: View {
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("\(brew ? "Homebrew cleanup is enabled" : "Homebrew cleanup is disabled")") Text("\(brew ? "Homebrew cleanup is enabled" : "Homebrew cleanup is disabled")")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and execute a brew uninstall and brew cleanup command as well to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force") InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and execute a brew uninstall and brew cleanup command as well to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force")
@@ -76,11 +76,11 @@ struct GeneralSettingsTab: View {
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("\(oneShotMode ? "One-Shot Mode is enabled" : "One-Shot Mode is disabled")") Text("\(oneShotMode ? "One-Shot Mode is enabled" : "One-Shot Mode is disabled")")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
InfoButton(text: "When one-shot mode is enabled, clicking the Uninstall button to remove an app will also close Pearcleaner right after. This only affects Pearcleaner when it is opened via external means, like Sentinel Trash Monitor or the Finder extension. This allows for single use of the app, AKA one-shot mode. When Pearcleaner is opened normally, this setting is ignored and will work as usual.") InfoButton(text: "When one-shot mode is enabled, clicking the Uninstall button to remove an app will also close Pearcleaner right after. This only affects Pearcleaner when it is opened via external means, like Sentinel Trash Monitor or the Finder extension. This allows for single use of the app, AKA one-shot mode. When Pearcleaner is opened normally, this setting is ignored and will work as usual.")
@@ -100,11 +100,11 @@ struct GeneralSettingsTab: View {
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("File list sorting mode") Text("File list sorting mode")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
InfoButton(text: "When searching for app files or leftover files, the list will be sorted either alphabetically or by size(large to small)") InfoButton(text: "When searching for app files or leftover files, the list will be sorted either alphabetically or by size(large to small)")
Spacer() Spacer()
@@ -114,7 +114,7 @@ struct GeneralSettingsTab: View {
Text("Size") Text("Size")
.tag(false) .tag(false)
} }
.pickerStyle(themeSettings: themeSettings) .buttonStyle(.borderless)
} }
.padding(5) .padding(5)
.padding(.leading) .padding(.leading)
@@ -126,11 +126,11 @@ struct GeneralSettingsTab: View {
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("File size display mode") Text("File size display mode")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
InfoButton(text: "Real size type will show how much actual allocated space the file has on disk. Logical type shows the binary size. The filesystem can compress and deduplicate sectors on disk, so real size is sometimes smaller(or bigger) than logical size. Finder size is similar to if you right click > Get Info on a file in Finder, which will show both the logical and real sizes together.") InfoButton(text: "Real size type will show how much actual allocated space the file has on disk. Logical type shows the binary size. The filesystem can compress and deduplicate sectors on disk, so real size is sometimes smaller(or bigger) than logical size. Finder size is similar to if you right click > Get Info on a file in Finder, which will show both the logical and real sizes together.")
Spacer() Spacer()
@@ -142,8 +142,7 @@ struct GeneralSettingsTab: View {
Text("Finder") Text("Finder")
.tag("Finder") .tag("Finder")
} }
.pickerStyle(themeSettings: themeSettings) .buttonStyle(.borderless)
} }
.padding(5) .padding(5)
@@ -153,120 +152,116 @@ struct GeneralSettingsTab: View {
// === Perms ================================================================================================ // === Perms ================================================================================================
Divider() // Divider()
.padding() // .padding()
// HStack() {
// Text("Permissions").font(.title2)
//
//// InfoButtonPerms()
//
// Spacer()
//
// Button("Refresh") {
// checkAllPermissions(appState: appState) { results in
// updateOnMain {
// appState.permissionResults = results
// }
// diskStatus = results.fullDiskAccess
// accessStatus = results.accessibility
// autoStatus = results.automation
// if results.allPermissionsGranted {
// updateOnMain {
// appState.permissionsOkay = true
// }
// }
// }
// }
// .buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", label: "Refresh", help: "Refresh permissions"))
// .padding(.trailing, 5)
//
// }
// .padding(.leading)
HStack() { // HStack(spacing: 0) {
Text("Permissions").font(.title2) // Image(systemName: "externaldrive")
// .resizable()
InfoButtonPerms() // .scaledToFit()
// .frame(width: 20, height: 20)
Spacer() // .padding(.trailing)
// .foregroundStyle(diskStatus ? .green : .red)
Button("Refresh") { // .saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
checkAllPermissions(appState: appState) { results in // Text(diskStatus ? "Full Disk permission granted" : "Full Disk permission not granted")
updateOnMain { // .font(.callout)
appState.permissionResults = results // .foregroundStyle(.primary.opacity(0.5))
} //
diskStatus = results.fullDiskAccess // Spacer()
accessStatus = results.accessibility //
autoStatus = results.automation // Button("") {
if results.allPermissionsGranted { // if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
updateOnMain { // NSWorkspace.shared.open(url)
appState.permissionsOkay = true // }
} // }
} // .buttonStyle(SimpleButtonStyle(icon: "folder", help: "View disk permissions pane"))
} //
} // }
.buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", label: "Refresh", help: "Refresh permissions")) // .padding(5)
.padding(.trailing, 5) // .padding(.leading)
//
} //
.padding(.leading) //
// HStack(spacing: 0) {
// Image(systemName: isVersionOrHigher(version: 14) ? "accessibility" : "figure.arms.open")
HStack(spacing: 0) { // .resizable()
Image(systemName: "externaldrive") // .scaledToFit()
.resizable() // .frame(width: 20, height: 20)
.scaledToFit() // .padding(.trailing)
.frame(width: 20, height: 20) // .foregroundStyle(accessStatus ? .green : .red)
.padding(.trailing) // .saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
.foregroundStyle(diskStatus ? .green : .red) // Text(accessStatus ? "Accessibility permission granted" : "Accessibility permission not granted")
.saturation(displayMode.colorScheme == .dark ? 0.8 : 1) // .font(.callout)
Text(diskStatus ? "Full Disk permission granted" : "Full Disk permission not granted") // .foregroundStyle(.primary.opacity(0.5))
.font(.callout) //
.foregroundStyle(Color("mode").opacity(0.5)) // Spacer()
//
Spacer() // Button("") {
// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
Button("") { // NSWorkspace.shared.open(url)
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { // }
NSWorkspace.shared.open(url) // }
} // .buttonStyle(SimpleButtonStyle(icon: "folder", help: "View accessibility permissions pane"))
} //
.buttonStyle(SimpleButtonStyle(icon: "folder", help: "View disk permissions pane")) // }
// .padding(5)
} // .padding(.leading)
.padding(5) //
.padding(.leading) //
// HStack(spacing: 0) {
Spacer() // Image(systemName: "gearshape.2")
// .resizable()
// .scaledToFit()
HStack(spacing: 0) { // .frame(width: 20, height: 20)
Image(systemName: isMacOS14OrHigher() ? "accessibility" : "figure.arms.open") // .padding(.trailing)
.resizable() // .foregroundStyle(autoStatus ? .green : .red)
.scaledToFit() // .saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
.frame(width: 20, height: 20) // Text(autoStatus ? "Automation permission granted" : "Automation permission not granted")
.padding(.trailing) // .font(.callout)
.foregroundStyle(accessStatus ? .green : .red) // .foregroundStyle(.primary.opacity(0.5))
.saturation(displayMode.colorScheme == .dark ? 0.8 : 1) //
Text(accessStatus ? "Accessibility permission granted" : "Accessibility permission not granted") // Spacer()
.font(.callout) //
.foregroundStyle(Color("mode").opacity(0.5)) // Button("") {
// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation") {
Spacer() // NSWorkspace.shared.open(url)
// }
Button("") { // }
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { // .buttonStyle(SimpleButtonStyle(icon: "folder", help: "View automation permissions pane"))
NSWorkspace.shared.open(url) //
} // }
} // .padding(5)
.buttonStyle(SimpleButtonStyle(icon: "folder", help: "View accessibility permissions pane")) // .padding(.leading)
}
.padding(5)
.padding(.leading)
Spacer()
HStack(spacing: 0) {
Image(systemName: "gearshape.2")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(autoStatus ? .green : .red)
.saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
Text(autoStatus ? "Automation permission granted" : "Automation permission not granted")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
Spacer()
Button("") {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation") {
NSWorkspace.shared.open(url)
}
}
.buttonStyle(SimpleButtonStyle(icon: "folder", help: "View automation permissions pane"))
}
.padding(5)
.padding(.leading)
@@ -291,7 +286,7 @@ struct GeneralSettingsTab: View {
.saturation(displayMode.colorScheme == .dark ? 0.8 : 1) .saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
Text(sentinel ? "Detecting when apps are moved to Trash" : "**NOT** detecting when apps are moved to Trash") Text(sentinel ? "Detecting when apps are moved to Trash" : "**NOT** detecting when apps are moved to Trash")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
Spacer() Spacer()
Toggle(isOn: $sentinel, label: { Toggle(isOn: $sentinel, label: {
@@ -332,7 +327,7 @@ struct GeneralSettingsTab: View {
.saturation(displayMode.colorScheme == .dark ? 0.8 : 1) .saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
Text(appState.finderExtensionEnabled ? "Context menu extension for Finder is enabled" : "Context menu extension for Finder is disabled") Text(appState.finderExtensionEnabled ? "Context menu extension for Finder is enabled" : "Context menu extension for Finder is disabled")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
InfoButton(text: "Enabling the extension will allow you to right click apps in Finder and quickly uninstall them with Pearcleaner") InfoButton(text: "Enabling the extension will allow you to right click apps in Finder and quickly uninstall them with Pearcleaner")
Spacer() Spacer()
@@ -362,33 +357,14 @@ struct GeneralSettingsTab: View {
} }
.padding(.vertical, 5) .padding(.vertical, 5)
Spacer()
} }
.onAppear { .onAppear {
checkAllPermissions(appState: appState) { results in
updateOnMain {
appState.permissionResults = results
}
diskStatus = results.fullDiskAccess
accessStatus = results.accessibility
autoStatus = results.automation
if results.allPermissionsGranted {
updateOnMain {
appState.permissionsOkay = true
}
}
}
appState.updateExtensionStatus() appState.updateExtensionStatus()
} }
} }
.padding(20) .padding(20)
.frame(width: 500, height: 710) .frame(width: 500)//, height: 710)
} }
+226 -204
View File
@@ -9,24 +9,25 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import ServiceManagement import ServiceManagement
import AlinFoundation
struct InterfaceSettingsTab: View { struct InterfaceSettingsTab: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager @EnvironmentObject var fsm: FolderSettingsManager
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
@State private var windowSettings = WindowSettings() @State private var windowSettings = WindowSettings()
@Environment(\.colorScheme) var colorScheme: ColorScheme @Environment(\.colorScheme) var colorScheme: ColorScheme
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("displayMode") var displayMode: DisplayMode = .system // @AppStorage("displayMode") var displayMode: DisplayMode = .system
@AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general // @AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general
@AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.glass") private var glass: Bool = false
@AppStorage("settings.general.dark") var isDark: Bool = true @AppStorage("settings.general.dark") var isDark: Bool = true
@AppStorage("settings.interface.themesEnabled") var themesEnabled: Bool = false // @AppStorage("settings.interface.themesEnabled") var themesEnabled: Bool = false
@AppStorage("settings.general.popover") private var popoverStay: Bool = true @AppStorage("settings.general.popover") private var popoverStay: Bool = true
@AppStorage("settings.general.miniview") private var miniView: Bool = true @AppStorage("settings.general.miniview") private var miniView: Bool = true
@AppStorage("settings.general.selectedTheme") var selectedTheme: String = "Auto" // @AppStorage("settings.general.selectedTheme") var selectedTheme: String = "Auto"
@AppStorage("settings.interface.selectedMenubarIcon") var selectedMenubarIcon: String = "pear-4" @AppStorage("settings.interface.selectedMenubarIcon") var selectedMenubarIcon: String = "pear-4"
@State private var isLaunchAtLoginEnabled: Bool = false @State private var isLaunchAtLoginEnabled: Bool = false
@Binding var showPopover: Bool @Binding var showPopover: Bool
@@ -51,11 +52,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("\(glass ? "Transparent material enabled" : "Transparent material disabled")") Text("\(glass ? "Transparent material enabled" : "Transparent material disabled")")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
Spacer() Spacer()
Toggle(isOn: $glass, label: { Toggle(isOn: $glass, label: {
@@ -69,194 +70,216 @@ struct InterfaceSettingsTab: View {
.padding(.leading) .padding(.leading)
HStack(spacing: 0) {
Image(systemName: themesEnabled ? "paintpalette.fill" : "paintpalette")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(themesEnabled ? "Custom theming is enabled" : "Custom theming is disabled")")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
Spacer()
Toggle(isOn: $themesEnabled, label: {
})
.toggleStyle(.switch)
.onChange(of: themesEnabled) { newVal in
themeSettings.resetToDefault(dark: isDarkMode())
if isDarkMode() {
displayMode.colorScheme = .dark
} else {
displayMode.colorScheme = .light
}
if newVal {
selectedTheme = isDarkMode() ? "Dark" : "Light"
} else {
selectedTheme = "Auto"
}
}
}
.padding(5)
.padding(.leading)
/// Show theme color selector if theming is enabled
if themesEnabled {
HStack(spacing: 0) { HStack(spacing: 0) {
Image(systemName: "paintbrush") Image(systemName: "paintbrush")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("Application theme color") Text("Theme Mode")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
Spacer() Spacer()
ThemeSettingsView(opacity: 1)
.onChange(of: themeManager.displayMode) { _ in
HStack(spacing: 10) {
Button("") {
themeSettings.setPreset(preset: "pearcleaner", colorScheme: displayMode)
}
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "pearcleaner", colorScheme: displayMode), label: "Pearcleaner"))
Button("") {
themeSettings.setPreset(preset: "dracula", colorScheme: displayMode)
}
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "dracula", colorScheme: displayMode), label: "Dracula"))
Button("") {
themeSettings.setPreset(preset: "solarized", colorScheme: displayMode)
}
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "solarized", colorScheme: displayMode), label: "Solarized"))
Button("") {
themeSettings.setPreset(preset: "macOS", colorScheme: displayMode)
}
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "macOS", colorScheme: displayMode), label: "macOS"))
}
Spacer()
ColorPicker("", selection: $themeSettings.themeColor, supportsOpacity: false)
.onChange(of: themeSettings.themeColor) { newValue in
themeSettings.saveThemeColor()
}
.padding(.horizontal, 5)
Button("") {
themeSettings.resetToDefault(dark: isDarkMode())
if isDarkMode() {
displayMode.colorScheme = .dark
} else {
displayMode.colorScheme = .light
}
selectedTheme = isDarkMode() ? "Dark" : "Light"
}
.buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", help: "Reset color to default"))
}
.padding(5)
.padding(.leading)
}
HStack(spacing: 0) {
Image(systemName: {
switch displayMode {
case .dark:
return "moon.fill"
case .light:
return "sun.max.fill"
case .system:
return "circle.righthalf.filled"
}
}())
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text(themesEnabled ? "Application font color" : "Application color mode")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
InfoButton(text: "When custom theming is disabled, you can set the application color mode to follow the operating system using Auto. Or manually set it to Light or Dark. When custom theming is enabled, this selector acts mainly as a font color picker between light or dark to match your custom theme color.")
Spacer()
Picker("", selection: $selectedTheme) {
if !themesEnabled {
Text("Auto")
.tag("Auto")
}
Text(themesEnabled ? "Light" : "Dark")
.tag("Dark")
Text(themesEnabled ? "Dark" : "Light")
.tag("Light")
}
.pickerStyle(themeSettings: themeSettings)
.onChange(of: selectedTheme) { newTheme in
switch newTheme {
case "Auto":
if isDarkMode() {
displayMode.colorScheme = .dark
} else {
displayMode.colorScheme = .light
}
if !themesEnabled {
themeSettings.resetToDefault(dark: isDarkMode())
}
// Refresh foreground colors
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.selectedTab = .interface
}
if menubarEnabled{
MenuBarExtraManager.shared.restartMenuBarExtra() MenuBarExtraManager.shared.restartMenuBarExtra()
} }
case "Dark":
displayMode.colorScheme = .dark
if !themesEnabled {
themeSettings.resetToDefault(dark: true)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.selectedTab = .interface
}
if menubarEnabled{
MenuBarExtraManager.shared.restartMenuBarExtra()
}
case "Light":
displayMode.colorScheme = .light
if !themesEnabled {
themeSettings.resetToDefault(dark: false)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.selectedTab = .interface
}
if menubarEnabled{
MenuBarExtraManager.shared.restartMenuBarExtra()
}
default:
break
}
}
} }
.padding(5) .padding(5)
.padding(.leading) .padding(.leading)
// HStack(spacing: 0) {
// Image(systemName: themesEnabled ? "paintpalette.fill" : "paintpalette")
// .resizable()
// .scaledToFit()
// .frame(width: 20, height: 20)
// .padding(.trailing)
// .foregroundStyle(.primary.opacity(0.5))
// VStack(alignment: .leading, spacing: 5) {
// Text("\(themesEnabled ? "Custom theming is enabled" : "Custom theming is disabled")")
// .font(.callout)
// .foregroundStyle(.primary.opacity(0.5))
// }
// Spacer()
// Toggle(isOn: $themesEnabled, label: {
// })
// .toggleStyle(.switch)
// .onChange(of: themesEnabled) { newVal in
// themeManager.resetToDefault(dark: isDarkMode())
// if isDarkMode() {
// displayMode.colorScheme = .dark
// } else {
// displayMode.colorScheme = .light
// }
// if newVal {
// selectedTheme = isDarkMode() ? "Dark" : "Light"
// } else {
// selectedTheme = "Auto"
// }
// }
// }
// .padding(5)
// .padding(.leading)
//
//
// /// Show theme color selector if theming is enabled
// if themesEnabled {
// HStack(spacing: 0) {
// Image(systemName: "paintbrush")
// .resizable()
// .scaledToFit()
// .frame(width: 20, height: 20)
// .padding(.trailing)
// .foregroundStyle(.primary.opacity(0.5))
// VStack(alignment: .leading, spacing: 5) {
// Text("Application theme color")
// .font(.callout)
// .foregroundStyle(.primary.opacity(0.5))
// }
//
// Spacer()
//
//
// HStack(spacing: 10) {
// Button("") {
// themeManager.setPreset(preset: "pearcleaner", colorScheme: displayMode)
// }
//// .buttonStyle(PresetColor(fillColor: themeManager.getColorForPreset(preset: "pearcleaner", colorScheme: displayMode), label: "Pearcleaner"))
//
// Button("") {
// themeManager.setPreset(preset: "dracula", colorScheme: displayMode)
// }
//// .buttonStyle(PresetColor(fillColor: themeManager.getColorForPreset(preset: "dracula", colorScheme: displayMode), label: "Dracula"))
//
// Button("") {
// themeManager.setPreset(preset: "solarized", colorScheme: displayMode)
// }
//// .buttonStyle(PresetColor(fillColor: themeManager.getColorForPreset(preset: "solarized", colorScheme: displayMode), label: "Solarized"))
//
// Button("") {
// themeManager.setPreset(preset: "macOS", colorScheme: displayMode)
// }
//// .buttonStyle(PresetColor(fillColor: themeManager.getColorForPreset(preset: "macOS", colorScheme: displayMode), label: "macOS"))
// }
//
//
//
//
// Spacer()
//
// ColorPicker("", selection: $themeManager.themeColor, supportsOpacity: false)
// .onChange(of: themeManager.themeColor) { newValue in
// themeManager.saveThemeColor()
// }
// .padding(.horizontal, 5)
//
// Button("") {
// themeManager.resetToDefault(dark: isDarkMode())
// if isDarkMode() {
// displayMode.colorScheme = .dark
// } else {
// displayMode.colorScheme = .light
// }
// selectedTheme = isDarkMode() ? "Dark" : "Light"
// }
// .buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", help: "Reset color to default"))
//
// }
// .padding(5)
// .padding(.leading)
// }
// HStack(spacing: 0) {
// Image(systemName: {
// switch displayMode {
// case .dark:
// return "moon.fill"
// case .light:
// return "sun.max.fill"
// case .system:
// return "circle.righthalf.filled"
// }
// }())
// .resizable()
// .scaledToFit()
// .frame(width: 20, height: 20)
// .padding(.trailing)
// .foregroundStyle(.primary.opacity(0.5))
// VStack(alignment: .leading, spacing: 5) {
// Text(themesEnabled ? "Application font color" : "Application color mode")
// .font(.callout)
// .foregroundStyle(.primary.opacity(0.5))
// }
// InfoButton(text: "When custom theming is disabled, you can set the application color mode to follow the operating system using Auto. Or manually set it to Light or Dark. When custom theming is enabled, this selector acts mainly as a font color picker between light or dark to match your custom theme color.")
// Spacer()
// Picker("", selection: $selectedTheme) {
// if !themesEnabled {
// Text("Auto")
// .tag("Auto")
// }
// Text(themesEnabled ? "Light" : "Dark")
// .tag("Dark")
// Text(themesEnabled ? "Dark" : "Light")
// .tag("Light")
// }
// .buttonStyle(.borderless)
// .onChange(of: selectedTheme) { newTheme in
// switch newTheme {
// case "Auto":
// if isDarkMode() {
// displayMode.colorScheme = .dark
// } else {
// displayMode.colorScheme = .light
// }
// if !themesEnabled {
// themeManager.resetToDefault(dark: isDarkMode())
// }
// // Refresh foreground colors
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// self.selectedTab = .interface
// }
// if menubarEnabled{
// MenuBarExtraManager.shared.restartMenuBarExtra()
// }
// case "Dark":
// displayMode.colorScheme = .dark
// if !themesEnabled {
// themeManager.resetToDefault(dark: true)
// }
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// self.selectedTab = .interface
// }
// if menubarEnabled{
// MenuBarExtraManager.shared.restartMenuBarExtra()
// }
// case "Light":
// displayMode.colorScheme = .light
// if !themesEnabled {
// themeManager.resetToDefault(dark: false)
// }
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// self.selectedTab = .interface
// }
// if menubarEnabled{
// MenuBarExtraManager.shared.restartMenuBarExtra()
// }
// default:
// break
// }
// }
// }
// .padding(5)
// .padding(.leading)
// === Mini ================================================================================================= // === Mini =================================================================================================
Divider() Divider()
@@ -271,16 +294,16 @@ struct InterfaceSettingsTab: View {
.padding(.leading) .padding(.leading)
HStack(spacing: 0) { HStack(spacing: 0) {
Image(systemName: isMacOS14OrHigher() ? "square.resize.up" : "macwindow.on.rectangle") Image(systemName: isVersionOrHigher(version: 14) ? "square.resize.up" : "macwindow.on.rectangle")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("\(mini ? "Mini window mode" : "Full size window mode")") Text("\(mini ? "Mini window mode" : "Full size window mode")")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
Spacer() Spacer()
Toggle(isOn: $mini, label: { Toggle(isOn: $mini, label: {
@@ -297,8 +320,8 @@ struct InterfaceSettingsTab: View {
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm) .environmentObject(fsm)
.environmentObject(ThemeSettings.shared) .environmentObject(themeManager)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(themeManager.displayMode.colorScheme)
}) { }) {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner") resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
} }
@@ -313,8 +336,8 @@ struct InterfaceSettingsTab: View {
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm) .environmentObject(fsm)
.environmentObject(ThemeSettings.shared) .environmentObject(themeManager)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(themeManager.displayMode.colorScheme)
} }
) { ) {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner") resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
@@ -334,11 +357,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("\(miniView ? "Show apps list on startup" : "Show drop target on startup")") Text("\(miniView ? "Show apps list on startup" : "Show drop target on startup")")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
InfoButton(text: "In mini window mode, you can have Pearcleaner startup to the Apps List view or the Drop Target view.") InfoButton(text: "In mini window mode, you can have Pearcleaner startup to the Apps List view or the Drop Target view.")
@@ -361,11 +384,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("\(popoverStay ? "Popover window will stay on top" : "Popover window will not stay on top")") Text("\(popoverStay ? "Popover window will stay on top" : "Popover window will not stay on top")")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
InfoButton(text: "In mini window mode, if you pin the Files popover on top, clicking away from the window will not dismiss it. Otherwise, it will dismiss by clicking anywhere outside the popover.") InfoButton(text: "In mini window mode, if you pin the Files popover on top, clicking away from the window will not dismiss it. Otherwise, it will dismiss by clicking anywhere outside the popover.")
@@ -397,11 +420,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("\(menubarEnabled ? "Menubar icon enabled" : "Menubar icon disabled")") Text("\(menubarEnabled ? "Menubar icon enabled" : "Menubar icon disabled")")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
InfoButton(text: "When menubar icon is enabled, the main app window and dock icon will be disabled since the app will be put in accessory mode.") InfoButton(text: "When menubar icon is enabled, the main app window and dock icon will be disabled since the app will be put in accessory mode.")
Spacer() Spacer()
@@ -415,8 +438,8 @@ struct InterfaceSettingsTab: View {
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm) .environmentObject(fsm)
.environmentObject(ThemeSettings.shared) .environmentObject(themeManager)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(themeManager.displayMode.colorScheme)
}, icon: selectedMenubarIcon) }, icon: selectedMenubarIcon)
NSApplication.shared.setActivationPolicy(.accessory) NSApplication.shared.setActivationPolicy(.accessory)
findAndHideWindows(named: ["Pearcleaner"]) findAndHideWindows(named: ["Pearcleaner"])
@@ -431,8 +454,8 @@ struct InterfaceSettingsTab: View {
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm) .environmentObject(fsm)
.environmentObject(ThemeSettings.shared) .environmentObject(themeManager)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(themeManager.displayMode.colorScheme)
}) { }) {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner") resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
} }
@@ -442,8 +465,8 @@ struct InterfaceSettingsTab: View {
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm) .environmentObject(fsm)
.environmentObject(ThemeSettings.shared) .environmentObject(themeManager)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(themeManager.displayMode.colorScheme)
}) { }) {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner") resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
} }
@@ -464,11 +487,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("\(isLaunchAtLoginEnabled ? "Launch at login enabled" : "Launch at login disabled")") Text("\(isLaunchAtLoginEnabled ? "Launch at login enabled" : "Launch at login disabled")")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
InfoButton(text: "This setting will affect Pearcleaner whether you're running in menubar mode or regular mode. If you disable menubar icon, you might want to disable this as well so Pearcleaner doesn't start on login.") InfoButton(text: "This setting will affect Pearcleaner whether you're running in menubar mode or regular mode. If you disable menubar icon, you might want to disable this as well so Pearcleaner doesn't start on login.")
Spacer() Spacer()
@@ -505,11 +528,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("Menubar icon") Text("Menubar icon")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
Spacer() Spacer()
Picker("", selection: $selectedMenubarIcon) { Picker("", selection: $selectedMenubarIcon) {
@@ -533,19 +556,18 @@ struct InterfaceSettingsTab: View {
.onChange(of: selectedMenubarIcon) { newValue in .onChange(of: selectedMenubarIcon) { newValue in
MenuBarExtraManager.shared.swapMenuBarIcon(icon: newValue) MenuBarExtraManager.shared.swapMenuBarIcon(icon: newValue)
} }
.pickerStyle(themeSettings: themeSettings) .buttonStyle(.borderless)
} }
.padding(5) .padding(5)
.padding(.leading) .padding(.leading)
Spacer() // Spacer()
} }
} }
.padding(20) .padding(20)
.frame(width: 500, height: themesEnabled ? 600 : 580) .frame(width: 500)//, height: themesEnabled ? 600 : 580)
} }
+5 -2
View File
@@ -6,11 +6,13 @@
// //
import SwiftUI import SwiftUI
import AlinFoundation
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var fsm: FolderSettingsManager @EnvironmentObject var fsm: FolderSettingsManager
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var updater: Updater
@Binding var showPopover: Bool @Binding var showPopover: Bool
@Binding var search: String @Binding var search: String
@AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.glass") private var glass: Bool = false
@@ -42,6 +44,7 @@ struct SettingsView: View {
Label(CurrentTabView.update.title, systemImage: "cloud") Label(CurrentTabView.update.title, systemImage: "cloud")
} }
.tag(CurrentTabView.update) .tag(CurrentTabView.update)
.environmentObject(themeManager)
TipsSettingsTab() TipsSettingsTab()
.tabItem { .tabItem {
@@ -55,7 +58,7 @@ struct SettingsView: View {
} }
.tag(CurrentTabView.about) .tag(CurrentTabView.about)
} }
.background(backgroundView(themeSettings: themeSettings, glass: glass)) .background(backgroundView(themeManager: themeManager, glass: glass))
} }
+5 -4
View File
@@ -8,10 +8,11 @@
import SwiftUI import SwiftUI
import Foundation import Foundation
import AlinFoundation
struct TipsSettingsTab: View { struct TipsSettingsTab: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
var body: some View { var body: some View {
VStack { VStack {
@@ -37,16 +38,16 @@ struct TipsSettingsTab: View {
.padding() .padding()
} }
.scrollIndicators(.visible) .scrollIndicators(.visible)
.frame(minHeight: 0, maxHeight: .infinity) .frame(minHeight: 0, maxHeight: 500)
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
.background(backgroundView(themeSettings: themeSettings, darker: true)) .background(backgroundView(themeManager: themeManager, darker: true))
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
.padding(.bottom) .padding(.bottom)
} }
.padding(20) .padding(20)
.frame(width: 500, height: 520) .frame(width: 500)//, height: 520)
} }
} }
+16 -66
View File
@@ -8,82 +8,34 @@
import SwiftUI import SwiftUI
import Foundation import Foundation
import AlinFoundation
struct UpdateSettingsTab: View { struct UpdateSettingsTab: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var updater: Updater
@State private var showAlert = false @State private var showAlert = false
@State private var showDone = false @State private var showDone = false
@AppStorage("settings.updater.nextUpdateDate") private var nextUpdateDate = Date.now.timeIntervalSinceReferenceDate
@AppStorage("settings.updater.updateFrequency") private var updateFrequency: UpdateFrequency = .daily
var body: some View { var body: some View {
VStack { VStack {
VStack { FrequencyView(updater: updater)
HStack(spacing: 0) {
Text("Pearcleaner will check for updates")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
Spacer()
Picker("", selection: $updateFrequency) {
ForEach(UpdateFrequency.allCases, id: \.self) { frequency in
Text(frequency.rawValue).tag(frequency)
}
}
.onChange(of: updateFrequency) { frequency in
frequency.updateNextUpdateDate()
}
.pickerStyle(themeSettings: themeSettings)
}
if updateFrequency != .none {
HStack {
Text("Next update check: \(formattedDate(Date(timeIntervalSinceReferenceDate: nextUpdateDate)))")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.3))
Spacer()
}
}
}
.padding(5) .padding(5)
.padding(.horizontal) .padding(.horizontal)
ScrollView { ReleasesView(updater: updater)
VStack() { .frame(maxWidth: .infinity, maxHeight: 400)
ForEach(appState.releases, id: \.id) { release in .backgroundAF(opacity: 0.5)
VStack(alignment: .leading) {
LabeledDivider(label: "\(release.tag_name)")
Text(release.modifiedBody)
}
}
}
.padding()
}
.frame(minHeight: 0, maxHeight: .infinity)
.frame(minWidth: 0, maxWidth: .infinity)
// .background(Color("mode").opacity(0.05))
.background(backgroundView(themeSettings: themeSettings, darker: true))
.clipShape(RoundedRectangle(cornerRadius: 8))
.padding(.bottom)
Text("Showing last 3 releases")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
HStack(alignment: .center, spacing: 20) { HStack(alignment: .center, spacing: 20) {
Spacer() Spacer()
Button(""){ Button(""){
loadGithubReleases(appState: appState) updater.checkForUpdates(showSheet: false)
} }
.buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", help: "Reload release notes")) .buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", help: "Refresh updater"))
Spacer() Spacer()
@@ -94,12 +46,12 @@ struct UpdateSettingsTab: View {
Spacer() Spacer()
Button(""){ // Button(""){
loadGithubReleases(appState: appState, manual: true) // updater.checkForUpdates(showSheet: true)
} // }
.buttonStyle(SimpleButtonStyle(icon: "arrow.down.square", help: "Check for updates")) // .buttonStyle(SimpleButtonStyle(icon: "arrow.down.square", help: "Check for updates"))
//
Spacer() // Spacer()
Button(""){ Button(""){
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!) NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!)
@@ -110,11 +62,9 @@ struct UpdateSettingsTab: View {
} }
.padding() .padding()
} }
.padding(20) .padding(20)
.frame(width: 500, height: 520) .frame(width: 500)//, height: 520)
} }
} }
+9 -8
View File
@@ -7,10 +7,11 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
struct AppListItems: View { struct AppListItems: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
@Binding var search: String @Binding var search: String
@State private var isHovered = false @State private var isHovered = false
@State private var windowSettings = WindowSettings() @State private var windowSettings = WindowSettings()
@@ -60,21 +61,21 @@ struct AppListItems: View {
if appInfo.webApp { if appInfo.webApp {
Text("web") Text("web")
.font(.footnote) .font(.footnote)
.foregroundStyle(Color("mode").opacity(0.3)) .foregroundStyle(.primary.opacity(0.3))
.frame(minWidth: 30, minHeight: 15) .frame(minWidth: 30, minHeight: 15)
.padding(2) .padding(2)
.background( .background(
Capsule().strokeBorder(Color("mode").opacity(0.3), lineWidth: 1) Capsule().strokeBorder(.primary.opacity(0.3), lineWidth: 1)
) )
} }
if appInfo.wrapped { if appInfo.wrapped {
Text("iOS") Text("iOS")
.font(.footnote) .font(.footnote)
.foregroundStyle(Color("mode").opacity(0.3)) .foregroundStyle(.primary.opacity(0.3))
.frame(minWidth: 30, minHeight: 15) .frame(minWidth: 30, minHeight: 15)
.padding(2) .padding(2)
.background( .background(
Capsule().strokeBorder(Color("mode").opacity(0.3), lineWidth: 1) Capsule().strokeBorder(.primary.opacity(0.3), lineWidth: 1)
) )
} }
@@ -83,7 +84,7 @@ struct AppListItems: View {
} else { } else {
Text("\(isHovered ? "v\(appInfo.appVersion)" : formatByte(size: bundleSize).human)") Text("\(isHovered ? "v\(appInfo.appVersion)" : formatByte(size: bundleSize).human)")
.font(.system(size: (isHovered || isSelected) ? 12 : 10)) .font(.system(size: (isHovered || isSelected) ? 12 : 10))
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
} }
@@ -112,14 +113,14 @@ struct AppListItems: View {
} }
.background{ .background{
Rectangle() Rectangle()
.fill(isSelected && !glass ? themeSettings.themeColor : .clear) .fill(isSelected && !glass ? themeManager.pickerColor : .clear)
} }
.overlay{ .overlay{
if (isHovered || isSelected) { if (isHovered || isSelected) {
HStack { HStack {
Spacer() Spacer()
RoundedRectangle(cornerRadius: 50) RoundedRectangle(cornerRadius: 50)
.fill(isSelected ? Color("pear") : Color("mode").opacity(0.5)) .fill(isSelected ? Color("pear") : .primary.opacity(0.5))
.frame(width: isSelected ? 4 : 2, height: 25) .frame(width: isSelected ? 4 : 2, height: 25)
.padding(.trailing, 5) .padding(.trailing, 5)
} }
+27 -11
View File
@@ -6,12 +6,15 @@
// //
import SwiftUI import SwiftUI
import AlinFoundation
struct AppSearchView: View { struct AppSearchView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager @EnvironmentObject var fsm: FolderSettingsManager
@EnvironmentObject var updater: Updater
@EnvironmentObject var permissionManager: PermissionManager
var glass: Bool var glass: Bool
var sidebarWidth: Double var sidebarWidth: Double
var menubarEnabled: Bool var menubarEnabled: Bool
@@ -21,6 +24,7 @@ struct AppSearchView: View {
@State private var showMenu = false @State private var showMenu = false
@State private var showSys: Bool = true @State private var showSys: Bool = true
@State private var showUsr: Bool = true @State private var showUsr: Bool = true
@State private var showFeatures: Bool = false
@Binding var isMenuBar: Bool @Binding var isMenuBar: Bool
@AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true @AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true
@State private var progress: Double = 0.0 @State private var progress: Double = 0.0
@@ -32,14 +36,25 @@ struct AppSearchView: View {
.frame(height: 10) .frame(height: 10)
.padding(.top, !isMenuBar ? 25 : 0) .padding(.top, !isMenuBar ? 25 : 0)
if appState.updateAvailable { if updater.updateAvailable {
UpdateNotificationView(appState: appState) UpdateButton(updater: updater, dark: false, opacity: 1)
} else if !appState.permissionsOkay { .padding(.horizontal)
PermissionsNotificationView(appState: appState) } else if let _ = permissionManager.results, !permissionManager.allPermissionsGranted {
PermissionsView(dark: false, opacity: 1)
.padding(.horizontal)
} else if appState.featureAvailable { } else if appState.featureAvailable {
FeatureNotificationView(appState: appState) AlertNotification(label: "New Message!", icon: "star", buttonAction: {
showFeatures.toggle()
}, btnColor: Color.blue, opacity: 1, themeManager: themeManager)
.padding(.horizontal)
.sheet(isPresented: $showFeatures, content: {
FeatureView()
.environmentObject(themeManager)
})
} }
AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps) AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps)
Divider() Divider()
@@ -137,7 +152,7 @@ struct AppSearchView: View {
} }
.padding() .padding()
.background(backgroundView(themeSettings: themeSettings, glass: glass).padding(-80)) .background(backgroundView(themeManager: themeManager, glass: glass).padding(-80))
} }
} }
@@ -148,6 +163,7 @@ struct AppSearchView: View {
.padding(.horizontal, search.isEmpty ? 10 : 5) .padding(.horizontal, search.isEmpty ? 10 : 5)
.padding(.vertical, 5) .padding(.vertical, 5)
} }
} }
private var filteredApps: [AppInfo] { private var filteredApps: [AppInfo] {
@@ -181,7 +197,7 @@ struct SimpleSearchStyle: TextFieldStyle {
@State var padding: CGFloat = 5 @State var padding: CGFloat = 5
@State var sidebar: Bool = true @State var sidebar: Bool = true
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@@ -189,7 +205,7 @@ struct SimpleSearchStyle: TextFieldStyle {
ZStack { ZStack {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.fill(darker ? themeSettings.themeColor.darker(by: 5) : themeSettings.themeColor) .fill(darker ? themeManager.pickerColor.adjustBrightness(5) : themeManager.pickerColor)
.allowsHitTesting(false) .allowsHitTesting(false)
.frame(height: 30) .frame(height: 30)
.opacity((glass && (sidebar || !mini && !menubarEnabled)) || mini || menubarEnabled ? 0.0 : 1.0) .opacity((glass && (sidebar || !mini && !menubarEnabled)) || mini || menubarEnabled ? 0.0 : 1.0)
@@ -201,7 +217,7 @@ struct SimpleSearchStyle: TextFieldStyle {
Spacer() Spacer()
Text(isFocused ? "Type to search" : "Click to search") Text(isFocused ? "Type to search" : "Click to search")
.font(.subheadline) .font(.subheadline)
.foregroundColor(Color("mode").opacity(0.2)) .foregroundColor(.primary.opacity(0.2))
Spacer() Spacer()
} }
} }
@@ -243,7 +259,7 @@ struct SimpleSearchStyle: TextFieldStyle {
extension NSTextView { extension NSTextView {
open override var frame: CGRect { open override var frame: CGRect {
didSet { didSet {
insertionPointColor = NSColor(Color("mode").opacity(0.2))//.clear insertionPointColor = NSColor(.primary.opacity(0.2))//.clear
} }
} }
} }
+2 -2
View File
@@ -73,13 +73,13 @@ struct Header: View {
var body: some View { var body: some View {
HStack { HStack {
Text("\(title)").foregroundStyle(Color("mode")).opacity(0.5) Text("\(title)").foregroundStyle(.primary).opacity(0.5)
Text("\(count)") Text("\(count)")
.font(.system(size: 10)) .font(.system(size: 10))
.monospacedDigit() .monospacedDigit()
.frame(minWidth: count > 99 ? 30 : 24, minHeight: 17) .frame(minWidth: count > 99 ? 30 : 24, minHeight: 17)
.background(Color("mode").opacity(0.1)) .background(.primary.opacity(0.1))
.clipShape(.capsule) .clipShape(.capsule)
.padding(.leading, 2) .padding(.leading, 2)
@@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
struct ConditionBuilderView: View { struct ConditionBuilderView: View {
@Binding var showAlert:Bool @Binding var showAlert:Bool
+15 -14
View File
@@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
struct FilesView: View { struct FilesView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@@ -51,7 +52,7 @@ struct FilesView: View {
VStack { VStack {
Spacer() Spacer()
Text("Searching the file system").font(.title3) Text("Searching the file system").font(.title3)
.foregroundStyle((Color("mode").opacity(0.5))) .foregroundStyle((.primary.opacity(0.5)))
ProgressView() ProgressView()
.progressViewStyle(.linear) .progressViewStyle(.linear)
@@ -59,7 +60,7 @@ struct FilesView: View {
Text("\(elapsedTime)") Text("\(elapsedTime)")
.font(.title).monospacedDigit() .font(.title).monospacedDigit()
.foregroundStyle((Color("mode").opacity(0.5))) .foregroundStyle((.primary.opacity(0.5)))
.opacity(elapsedTime == 0 ? 0 : 1) .opacity(elapsedTime == 0 ? 0 : 1)
.contentTransition(.numericText()) .contentTransition(.numericText())
@@ -133,7 +134,7 @@ struct FilesView: View {
Text("\(appState.appInfo.bundleIdentifier)") Text("\(appState.appInfo.bundleIdentifier)")
.lineLimit(1) .lineLimit(1)
.font(.title3) .font(.title3)
.foregroundStyle((Color("mode").opacity(0.5))) .foregroundStyle((.primary.opacity(0.5)))
} }
.padding(.leading) .padding(.leading)
@@ -143,7 +144,7 @@ struct FilesView: View {
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk") Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
Text("\(appState.appInfo.fileSize.count == 1 ? "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) item" : "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) items")") Text("\(appState.appInfo.fileSize.count == 1 ? "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) item" : "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) items")")
.font(.callout).foregroundStyle((Color("mode").opacity(0.5))) .font(.callout).foregroundStyle((.primary.opacity(0.5)))
} }
} }
@@ -177,11 +178,11 @@ struct FilesView: View {
if appState.appInfo.webApp { if appState.appInfo.webApp {
Text("web") Text("web")
.font(.footnote) .font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
.frame(minWidth: 30, minHeight: 15) .frame(minWidth: 30, minHeight: 15)
.padding(2) .padding(2)
.padding(.horizontal, 2) .padding(.horizontal, 2)
.background(Color("mode").opacity(0.1)) .background(.primary.opacity(0.1))
.clipShape(.capsule) .clipShape(.capsule)
.help("This is a web app") .help("This is a web app")
@@ -190,11 +191,11 @@ struct FilesView: View {
if appState.appInfo.wrapped { if appState.appInfo.wrapped {
Text("iOS") Text("iOS")
.font(.footnote) .font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
.frame(minWidth: 30, minHeight: 15) .frame(minWidth: 30, minHeight: 15)
.padding(2) .padding(2)
.padding(.horizontal, 2) .padding(.horizontal, 2)
.background(Color("mode").opacity(0.1)) .background(.primary.opacity(0.1))
.clipShape(.capsule) .clipShape(.capsule)
.help("This is a wrapped iOS app") .help("This is a wrapped iOS app")
@@ -203,11 +204,11 @@ struct FilesView: View {
if appState.appInfo.arch != .empty { if appState.appInfo.arch != .empty {
Text("\(appState.appInfo.arch)") Text("\(appState.appInfo.arch)")
.font(.footnote) .font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
.frame(minWidth: 30, minHeight: 15) .frame(minWidth: 30, minHeight: 15)
.padding(2) .padding(2)
.padding(.horizontal, 2) .padding(.horizontal, 2)
.background(Color("mode").opacity(0.1)) .background(.primary.opacity(0.1))
.clipShape(.capsule) .clipShape(.capsule)
.help("This bundle's architecture is \(appState.appInfo.arch)") .help("This bundle's architecture is \(appState.appInfo.arch)")
@@ -215,11 +216,11 @@ struct FilesView: View {
Text(appState.appInfo.system ? "system" : "user") Text(appState.appInfo.system ? "system" : "user")
.font(.footnote) .font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
.frame(minWidth: 30, minHeight: 15) .frame(minWidth: 30, minHeight: 15)
.padding(2) .padding(2)
.padding(.horizontal, 2) .padding(.horizontal, 2)
.background(Color("mode").opacity(0.1)) .background(.primary.opacity(0.1))
.clipShape(.capsule) .clipShape(.capsule)
.help("This app is located in \(appState.appInfo.system ? "/Applications" : "\(home)")") .help("This app is located in \(appState.appInfo.system ? "/Applications" : "\(home)")")
} }
@@ -293,7 +294,7 @@ struct FilesView: View {
Spacer() Spacer()
InfoButton(text: "Always double-check the files/folders marked for removal. In some rare cases, Pearcleaner may find some unrelated files when app names are too similar.", color: Color("mode").opacity(0.5), warning: true, edge: .top) InfoButton(text: "Always double-check the files/folders marked for removal. In some rare cases, Pearcleaner may find some unrelated files when app names are too similar.", color: .primary.opacity(0.5), warning: true, edge: .top)
.padding(.top) .padding(.top)
@@ -470,7 +471,7 @@ struct FileDetailsItem: View {
VStack { VStack {
Spacer() Spacer()
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.fill(Color("mode").opacity(0.5)) .fill(.primary.opacity(0.5))
.frame(height: 1.5) .frame(height: 1.5)
.offset(y: 3) .offset(y: 3)
} }
+7 -6
View File
@@ -8,10 +8,11 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
struct MiniMode: View { struct MiniMode: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
@Binding var search: String @Binding var search: String
@State private var showSys: Bool = true @State private var showSys: Bool = true
@State private var showUsr: Bool = true @State private var showUsr: Bool = true
@@ -38,7 +39,7 @@ struct MiniMode: View {
} }
.frame(minWidth: 300, minHeight: 345) .frame(minWidth: 300, minHeight: 345)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
.background(backgroundView(themeSettings: themeSettings, glass: glass)) .background(backgroundView(themeManager: themeManager, glass: glass))
} }
} }
@@ -151,7 +152,7 @@ struct MiniAppView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
// @State private var animateGradient: Bool = false // @State private var animateGradient: Bool = false
@Binding var search: String @Binding var search: String
@State private var showSys: Bool = true @State private var showSys: Bool = true
@@ -176,7 +177,7 @@ struct MiniAppView: View {
ProgressView() { ProgressView() {
Text("Gathering app details") Text("Gathering app details")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
.padding(5) .padding(5)
} }
Spacer() Spacer()
@@ -190,7 +191,7 @@ struct MiniAppView: View {
.transition(.opacity) .transition(.opacity)
.frame(minWidth: 300, minHeight: 370) .frame(minWidth: 300, minHeight: 370)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
.background(backgroundView(themeSettings: themeSettings, glass: glass).padding(-80)) .background(backgroundView(themeManager: themeManager, glass: glass).padding(-80))
.transition(.opacity) .transition(.opacity)
.popover(isPresented: $showPopover, arrowEdge: .trailing) { .popover(isPresented: $showPopover, arrowEdge: .trailing) {
VStack { VStack {
@@ -204,7 +205,7 @@ struct MiniAppView: View {
} }
.interactiveDismissDisabled(popoverStay) .interactiveDismissDisabled(popoverStay)
.background(backgroundView(themeSettings: themeSettings, glass: glass).padding(-80)) .background(backgroundView(themeManager: themeManager, glass: glass).padding(-80))
.frame(width: 650, height: 500) .frame(width: 650, height: 500)
} }
@@ -122,7 +122,7 @@ extension View {
// content: { item, isSelected in // content: { item, isSelected in
// Text(item) // Text(item)
// .font(.callout) // .font(.callout)
// .foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5)) // .foregroundColor(isSelected ? .primary : .primary.opacity(0.5))
// .padding(.horizontal) // .padding(.horizontal)
// .padding(.bottom, 5) // .padding(.bottom, 5)
// .frame(width: 75) // .frame(width: 75)
@@ -161,7 +161,7 @@ extension View {
// content: { item, isSelected in // content: { item, isSelected in
// Text(item) // Text(item)
// .font(.callout) // .font(.callout)
// .foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5) ) // .foregroundColor(isSelected ? .primary : .primary.opacity(0.5) )
// .padding(.horizontal) // .padding(.horizontal)
// .padding(.bottom, 5) // .padding(.bottom, 5)
// .frame(width: 75) // .frame(width: 75)
@@ -198,7 +198,7 @@ extension View {
// content: { item, isSelected in // content: { item, isSelected in
// Text(item) // Text(item)
// .font(.callout) // .font(.callout)
// .foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5) ) // .foregroundColor(isSelected ? .primary : .primary.opacity(0.5) )
// .padding(.horizontal) // .padding(.horizontal)
// .padding(.bottom, 5) // .padding(.bottom, 5)
// .frame(width: 65) // .frame(width: 65)
+6 -5
View File
@@ -7,10 +7,11 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
struct RegularMode: View { struct RegularMode: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.glass") private var glass: Bool = false
@AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280 @AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280
@@ -36,7 +37,7 @@ struct RegularMode: View {
ProgressView() { ProgressView() {
Text("Gathering app details") Text("Gathering app details")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
.padding(5) .padding(5)
} }
Spacer() Spacer()
@@ -49,11 +50,11 @@ struct RegularMode: View {
} }
} }
.background(backgroundView(themeSettings: themeSettings, darker: true, glass: glass)) .background(backgroundView(themeManager: themeManager, darker: true, glass: glass))
.transition(.opacity) .transition(.opacity)
SlideableDivider(dimension: $sidebarWidth) SlideableDivider(dimension: $sidebarWidth)
.background(backgroundView(themeSettings: themeSettings)) .background(backgroundView(themeManager: themeManager))
.zIndex(3) .zIndex(3)
@@ -75,7 +76,7 @@ struct RegularMode: View {
Spacer() Spacer()
} }
.zIndex(2) .zIndex(2)
.background(backgroundView(themeSettings: themeSettings)) .background(backgroundView(themeManager: themeManager))
} }
.frame(minWidth: 900, minHeight: 600) .frame(minWidth: 900, minHeight: 600)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
+23 -11
View File
@@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AlinFoundation
struct ZombieView: View { struct ZombieView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@@ -45,7 +46,7 @@ struct ZombieView: View {
Spacer() Spacer()
Text("Searching the file system").font(.title3) Text("Searching the file system").font(.title3)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
ProgressView() ProgressView()
.progressViewStyle(.linear) .progressViewStyle(.linear)
@@ -54,7 +55,7 @@ struct ZombieView: View {
Text("\(elapsedTime)") Text("\(elapsedTime)")
.font(.title).monospacedDigit() .font(.title).monospacedDigit()
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
.opacity(elapsedTime == 0 ? 0 : 1) .opacity(elapsedTime == 0 ? 0 : 1)
.contentTransition(.numericText()) .contentTransition(.numericText())
@@ -116,7 +117,7 @@ struct ZombieView: View {
Spacer() Spacer()
} }
Text("Remaining files and folders from previous applications") Text("Remaining files and folders from previous applications")
.font(.callout).foregroundStyle(Color("mode").opacity(0.5)) .font(.callout).foregroundStyle(.primary.opacity(0.5))
} }
Spacer() Spacer()
@@ -125,7 +126,7 @@ struct ZombieView: View {
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk") Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
Text("\(selectedZombieItemsLocal.count) / \(searchZ.isEmpty ? appState.zombieFile.fileSize.count : memoizedFiles.count) \(appState.zombieFile.fileSize.count == 1 ? "item" : "items")") Text("\(selectedZombieItemsLocal.count) / \(searchZ.isEmpty ? appState.zombieFile.fileSize.count : memoizedFiles.count) \(appState.zombieFile.fileSize.count == 1 ? "item" : "items")")
.font(.callout).foregroundStyle(Color("mode").opacity(0.5)) .font(.callout).foregroundStyle(.primary.opacity(0.5))
} }
} }
@@ -258,17 +259,28 @@ struct ZombieView: View {
} }
} }
} } else {
updateOnMain {
// Remove items from the list // Remove items from the list
appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) } appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) }
// Update the selectedZombieFiles to remove references that are no longer present appState.zombieFile.fileSizeLogical = appState.zombieFile.fileSizeLogical.filter { !selectedZombieItemsLocal.contains($0.key) }
selectedZombieItemsLocal.removeAll() appState.zombieFile.fileIcon = appState.zombieFile.fileIcon.filter { !selectedZombieItemsLocal.contains($0.key) }
updateTotalSizes()
// Clear the selection
selectedZombieItemsLocal.removeAll()
// Update memoized files and total sizes
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
} }
// updateOnMain {
// // Remove items from the list
// appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) }
// // Update the selectedZombieFiles to remove references that are no longer present
// selectedZombieItemsLocal.removeAll()
// updateTotalSizes()
//
// }
} }
// updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true) // updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
@@ -472,7 +484,7 @@ struct ZombieFileDetailsItem: View {
VStack { VStack {
Spacer() Spacer()
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.fill(Color("mode").opacity(0.5)) .fill(.primary.opacity(0.5))
.frame(height: 1.5) .frame(height: 1.5)
.offset(y: 3) .offset(y: 3)
} }
+16 -10
View File
@@ -6,10 +6,11 @@
// //
import SwiftUI import SwiftUI
import AlinFoundation
struct FeatureView: View { struct FeatureView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var themeManager: ThemeManager
@Environment(\.dismiss) var dismiss
@AppStorage("settings.general.features") private var features: String = "" @AppStorage("settings.general.features") private var features: String = ""
var body: some View { var body: some View {
@@ -20,34 +21,39 @@ struct FeatureView: View {
Text("New features for v\((Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)!)!") .font(.title) Text("New features for v\((Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)!)!") .font(.title)
.bold() .bold()
.padding(.vertical) // .padding(.bottom)
Spacer() Spacer()
} }
Divider() Divider()
.padding([.horizontal]) .padding([.vertical])
Text(features) Text(features)
.font(.body) .font(.body)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.padding() .padding(.horizontal)
Spacer() Spacer()
HStack(alignment: .center, spacing: 20) { HStack(alignment: .center, spacing: 20) {
Button(action: { Button(action: {
NewWin.close() dismiss()
// NewWin.close()
}) { }) {
Text("Okay") Text("Close")
} }
.buttonStyle(SimpleButtonBrightStyle(icon: "checkmark.circle", label: "Ok", help: "Ok", color: Color("mode"))) // .buttonStyle(SimpleButtonBrightStyle(icon: "checkmark.circle", label: "Ok", help: "Ok", color: .primary))
} }
.padding(.bottom) .padding(.vertical)
} }
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0)) .padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(themeManager.pickerColor)
// .padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
} }
-9
View File
@@ -67,15 +67,6 @@ class NewWin: NSWindowController {
private func makeNewView(appState: AppState, newWin: NewWindow) -> some View { private func makeNewView(appState: AppState, newWin: NewWindow) -> some View {
switch newWin { switch newWin {
case .update:
return AnyView(UpdateView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.black.opacity(0.2))
.environmentObject(appState))
case .no_update:
return AnyView(NoUpdateView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.environmentObject(appState))
case .perm: case .perm:
return AnyView(PermView() return AnyView(PermView()
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
+11 -11
View File
@@ -39,10 +39,10 @@ struct PermView: View {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
Text("Full Disk Access permission to find and delete files in system paths") Text("Full Disk Access permission to find and delete files in system paths")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
HStack(alignment: .top, spacing: 20) { HStack(alignment: .top, spacing: 20) {
@@ -50,10 +50,10 @@ struct PermView: View {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
Text("Accessibility permission to delete files via Finder") Text("Accessibility permission to delete files via Finder")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
HStack(alignment: .top, spacing: 20) { HStack(alignment: .top, spacing: 20) {
@@ -61,10 +61,10 @@ struct PermView: View {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
Text("Automation permission to perform extension actions via Finder") Text("Automation permission to perform extension actions via Finder")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
HStack(alignment: .top, spacing: 20) { HStack(alignment: .top, spacing: 20) {
@@ -72,10 +72,10 @@ struct PermView: View {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
Text("Reminders permission to find and delete some reminder files that certain apps cache") Text("Reminders permission to find and delete some reminder files that certain apps cache")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
HStack(alignment: .top, spacing: 20) { HStack(alignment: .top, spacing: 20) {
@@ -83,10 +83,10 @@ struct PermView: View {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
Text("Add Pearcleaner in both Privacy panes via the + or by dragging the app over the pane. If the app is already pre-populated in the list, just toggle On if neeeded. Restart app when both permissions are granted") Text("Add Pearcleaner in both Privacy panes via the + or by dragging the app over the pane. If the app is already pre-populated in the list, just toggle On if neeeded. Restart app when both permissions are granted")
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(.primary.opacity(0.5))
} }
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
@@ -97,7 +97,7 @@ struct PermView: View {
if #available(macOS 14.0, *) { if #available(macOS 14.0, *) {
SettingsLink {} SettingsLink {}
.buttonStyle(SimpleButtonBrightStyle(icon: "gear", label: "Settings", help: "Check permissions in Settings", color: Color("mode"))) .buttonStyle(SimpleButtonBrightStyle(icon: "gear", label: "Settings", help: "Check permissions in Settings", color: .primary))
} else { } else {
Button("Settings") { Button("Settings") {
if #available(macOS 13.0, *) { if #available(macOS 13.0, *) {
-200
View File
@@ -1,200 +0,0 @@
//
// UpdateView.swift
// Pearcleaner
//
// Created by Alin Lupascu on 11/16/23.
//
import SwiftUI
struct UpdateView: View {
@EnvironmentObject var appState: AppState
@State private var isAppInCorrectDirectory: Bool = false
@State private var isUserAdmin: Bool = false
var body: some View {
VStack(spacing: 5) {
HStack {
Text("App: \(Bundle.main.version)")
.font(.title3)
.fontWeight(.semibold)
.opacity(0.5)
.padding()
Spacer()
Text("\(appState.progressBar.1 != 1.0 ? "Update Available 🥳" : "Completed 🚀")")
.font(.title)
.bold()
.padding(.vertical)
Spacer()
Text("GitHub: \(appState.releases.first?.tag_name ?? "")")
.font(.title3)
.fontWeight(.semibold)
.opacity(0.5)
.padding()
}
Divider()
.padding([.horizontal])
ScrollView {
Text("\(appState.releases.first?.modifiedBody ?? "No release information")")
.font(.body)
.multilineTextAlignment(.leading)
.padding(20)
}
Spacer()
if !isAppInCorrectDirectory {
Text("To avoid permission issues, please move Pearcleaner to the \(isUserAdmin ? "/Applications" : "\(home)/Applications") folder before updating!")
.font(.callout)
.foregroundStyle(Color("mode"))
.padding(.horizontal)
.padding(.top, 0)
}
VStack() {
ProgressView("\(appState.progressBar.0)", value: appState.progressBar.1, total: 1.0)
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
.frame(height: 10)
}
.padding()
HStack(alignment: .center, spacing: 20) {
Button(action: NewWin.close) {
Text("Cancel")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "x.circle", label: "Close", help: "Close", color: .red))
if appState.progressBar.1 != 1.0 {
Button(action: {
downloadUpdate(appState: appState)
}) {
Text("Update")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "arrow.down.circle", label: "Update", help: "Update", color: Color("mode")))
} else {
Button(action: {
NewWin.close()
relaunchApp(afterDelay: 1)
}) {
Text("Restart")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "arrow.uturn.left.circle", label: "Restart", help: "Restart", color: Color("mode")))
}
}
.padding(.bottom)
}
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
.onAppear {
checkAppDirectoryAndUserRole { result in
isAppInCorrectDirectory = result.isInCorrectDirectory
isUserAdmin = result.isAdmin
}
}
}
}
struct NoUpdateView: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack(spacing: 5) {
HStack {
Text("App: \(Bundle.main.version)")
.font(.title3)
.fontWeight(.semibold)
.opacity(0.5)
.padding()
Spacer()
Text("\(appState.progressBar.1 != 1.0 ? "No Update 😌" : "Completed 🚀")")
.font(.title)
.bold()
.padding(.vertical)
Spacer()
Text("GitHub: \(appState.releases.first?.tag_name ?? "")")
.font(.title3)
.fontWeight(.semibold)
.opacity(0.5)
.padding()
}
Divider()
.padding([.horizontal])
Text("Pearcleaner is on the most current release available, but you may force a re-download of the same version below.")
.font(.body)
.lineLimit(3)
.multilineTextAlignment(.leading)
.padding()
Spacer()
VStack() {
ProgressView("\(appState.progressBar.0)", value: appState.progressBar.1, total: 1.0)
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
.frame(height: 10)
}
.padding()
HStack(alignment: .center, spacing: 20) {
if appState.progressBar.1 < 0.1 {
Button(action: {
downloadUpdate(appState: appState)
}) {
Text("Force Update")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "arrow.down.circle", label: "Force Update", help: "Force-Update", color: .red))
Button(action: {
NewWin.close()
}) {
Text("Okay")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "checkmark.circle", label: "Ok", help: "Ok", color: Color("mode")))
}
if appState.progressBar.1 == 1.0 {
Button(action: {
NewWin.close()
relaunchApp()
}) {
Text("Restart")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "arrow.uturn.left.circle", label: "Restart", help: "Restart", color: Color("mode")))
}
}
.padding(.bottom)
}
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
}
}
+1
View File
@@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import AlinFoundation
class WindowSettings { class WindowSettings {
private let windowWidthKey = "windowWidthKey" private let windowWidthKey = "windowWidthKey"