diff --git a/PureMac.xcodeproj/project.pbxproj b/PureMac.xcodeproj/project.pbxproj index 1e7df29..907ea5d 100644 --- a/PureMac.xcodeproj/project.pbxproj +++ b/PureMac.xcodeproj/project.pbxproj @@ -7,46 +7,48 @@ objects = { /* Begin PBXBuildFile section */ + 013EF7B96BF046D4DB38FBC5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 241E0895B09C71AB423B2F9E /* Localizable.strings */; }; 015C7A8CE16D49F2C445C02A /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E751C3B9ED986F1E6D3C8F /* SettingsView.swift */; }; 0A7B70CBA747ED9FEE20C51A /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5402892366B3F00417FD05F9 /* SidebarView.swift */; }; + 2253F11BDF561B617439C96B /* FullDiskAccessManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E866A1541D289C69144A5E62 /* FullDiskAccessManager.swift */; }; 27F449EDD1B082FE11FEC9DF /* SchedulerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28181F034530331A550C6A3D /* SchedulerService.swift */; }; 340E424F759ACCDE7372F99F /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5ADDC35F31E40780FB5D017 /* Theme.swift */; }; 48D1431A7C99C19EEBDB056B /* CleaningEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8DE5D45BA19E2670B57DC5 /* CleaningEngine.swift */; }; + 4F754D89F4CE5142BE384062 /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F91226CDCFBB8E303B7DA /* AppConstants.swift */; }; 826A750D2D7EC14C2AE306A3 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3667D46D8E2004EB4D73835A /* Models.swift */; }; A221744A723582A6D075A7A0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86105573F4051764E9E46626 /* ContentView.swift */; }; A89DF967EC9E5E8123B2925A /* SmartScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11BA091943263913276F3CCF /* SmartScanView.swift */; }; - AA0000000000000000000004 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000003 /* Localizable.strings */; }; B52938BBD11842631314543D /* CategoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10B0AF194677EAC1D5568785 /* CategoryDetailView.swift */; }; B8A524AF7F7DCDFCFCA9EDF6 /* AppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FDA1B749FFA61DFB52AB5 /* AppViewModel.swift */; }; + BC6C800216343438413349A3 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D4FD34378988D430A582ED0 /* OnboardingView.swift */; }; D9445C2641A8637B65DA5ACE /* ScanEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A711CDF5285F68775D9B5513 /* ScanEngine.swift */; }; - D9D4E0FA2F8C135400ABB13C /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D4E0F92F8C135400ABB13C /* AppConstants.swift */; }; DDD6BA35DBF32E7A6B5F6F8B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B2EA41E1096FA8E3B916AD13 /* Assets.xcassets */; }; EDEF28CAD23E936FBED1783B /* PureMacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63581B70F9B10231964E3602 /* PureMacApp.swift */; }; - F1A2B3C4D5E6F70819203142 /* FullDiskAccessManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F607182930A1B2 /* FullDiskAccessManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 01E751C3B9ED986F1E6D3C8F /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 02E502E2B5C6AECC76E5CFEF /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 10B0AF194677EAC1D5568785 /* CategoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailView.swift; sourceTree = ""; }; 11BA091943263913276F3CCF /* SmartScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartScanView.swift; sourceTree = ""; }; + 2641C6376DD6F5889F35510E /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 28181F034530331A550C6A3D /* SchedulerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerService.swift; sourceTree = ""; }; 2A8DE5D45BA19E2670B57DC5 /* CleaningEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleaningEngine.swift; sourceTree = ""; }; 311078221878708524283765 /* PureMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PureMac.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3667D46D8E2004EB4D73835A /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 3D4FD34378988D430A582ED0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 46660271CFF167AB0FE7371D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 5402892366B3F00417FD05F9 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; 5664D2BDAEAA9AE3A53DB364 /* PureMac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PureMac.entitlements; sourceTree = ""; }; 63581B70F9B10231964E3602 /* PureMacApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PureMacApp.swift; sourceTree = ""; }; 86105573F4051764E9E46626 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - A1B2C3D4E5F607182930A1B2 /* FullDiskAccessManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullDiskAccessManager.swift; sourceTree = ""; }; + 9F04B811BB0012F6D2F07F91 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; A711CDF5285F68775D9B5513 /* ScanEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanEngine.swift; sourceTree = ""; }; - AA0000000000000000000001 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - AA0000000000000000000002 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; - AA0000000000000000000005 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; B2EA41E1096FA8E3B916AD13 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C5ADDC35F31E40780FB5D017 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; - D9D4E0F92F8C135400ABB13C /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; DD7FDA1B749FFA61DFB52AB5 /* AppViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewModel.swift; sourceTree = ""; }; + E866A1541D289C69144A5E62 /* FullDiskAccessManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullDiskAccessManager.swift; sourceTree = ""; }; + F31F91226CDCFBB8E303B7DA /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXGroup section */ @@ -79,6 +81,7 @@ children = ( 10B0AF194677EAC1D5568785 /* CategoryDetailView.swift */, 86105573F4051764E9E46626 /* ContentView.swift */, + 3D4FD34378988D430A582ED0 /* OnboardingView.swift */, 01E751C3B9ED986F1E6D3C8F /* SettingsView.swift */, 5402892366B3F00417FD05F9 /* SidebarView.swift */, 11BA091943263913276F3CCF /* SmartScanView.swift */, @@ -90,7 +93,7 @@ isa = PBXGroup; children = ( 2A8DE5D45BA19E2670B57DC5 /* CleaningEngine.swift */, - A1B2C3D4E5F607182930A1B2 /* FullDiskAccessManager.swift */, + E866A1541D289C69144A5E62 /* FullDiskAccessManager.swift */, A711CDF5285F68775D9B5513 /* ScanEngine.swift */, 28181F034530331A550C6A3D /* SchedulerService.swift */, ); @@ -113,10 +116,10 @@ path = ViewModels; sourceTree = ""; }; - D9D4E0F82F8C135000ABB13C /* Core */ = { + D4333B07691BD85CAE0E5B15 /* Core */ = { isa = PBXGroup; children = ( - D9D4E0F92F8C135400ABB13C /* AppConstants.swift */, + F31F91226CDCFBB8E303B7DA /* AppConstants.swift */, ); path = Core; sourceTree = ""; @@ -124,13 +127,13 @@ E383F69184F4552E5A41D010 /* PureMac */ = { isa = PBXGroup; children = ( - D9D4E0F82F8C135000ABB13C /* Core */, B2EA41E1096FA8E3B916AD13 /* Assets.xcassets */, - AA0000000000000000000003 /* Localizable.strings */, 46660271CFF167AB0FE7371D /* Info.plist */, 5664D2BDAEAA9AE3A53DB364 /* PureMac.entitlements */, 63581B70F9B10231964E3602 /* PureMacApp.swift */, + D4333B07691BD85CAE0E5B15 /* Core */, 7C1729F88C0E5563E1A3DB40 /* Extensions */, + 241E0895B09C71AB423B2F9E /* Localizable.strings */, 3CF46713F75B81F0F86D1C6F /* Models */, 6184B2EC3D01E6E95633406E /* Services */, A8F1C251567E5321E1CA2484 /* ViewModels */, @@ -202,7 +205,7 @@ buildActionMask = 2147483647; files = ( DDD6BA35DBF32E7A6B5F6F8B /* Assets.xcassets in Resources */, - AA0000000000000000000004 /* Localizable.strings in Resources */, + 013EF7B96BF046D4DB38FBC5 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -213,18 +216,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4F754D89F4CE5142BE384062 /* AppConstants.swift in Sources */, B8A524AF7F7DCDFCFCA9EDF6 /* AppViewModel.swift in Sources */, B52938BBD11842631314543D /* CategoryDetailView.swift in Sources */, 48D1431A7C99C19EEBDB056B /* CleaningEngine.swift in Sources */, A221744A723582A6D075A7A0 /* ContentView.swift in Sources */, + 2253F11BDF561B617439C96B /* FullDiskAccessManager.swift in Sources */, 826A750D2D7EC14C2AE306A3 /* Models.swift in Sources */, - F1A2B3C4D5E6F70819203142 /* FullDiskAccessManager.swift in Sources */, + BC6C800216343438413349A3 /* OnboardingView.swift in Sources */, EDEF28CAD23E936FBED1783B /* PureMacApp.swift in Sources */, D9445C2641A8637B65DA5ACE /* ScanEngine.swift in Sources */, 27F449EDD1B082FE11FEC9DF /* SchedulerService.swift in Sources */, 015C7A8CE16D49F2C445C02A /* SettingsView.swift in Sources */, 0A7B70CBA747ED9FEE20C51A /* SidebarView.swift in Sources */, - D9D4E0FA2F8C135400ABB13C /* AppConstants.swift in Sources */, A89DF967EC9E5E8123B2925A /* SmartScanView.swift in Sources */, 340E424F759ACCDE7372F99F /* Theme.swift in Sources */, ); @@ -233,12 +237,12 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - AA0000000000000000000003 /* Localizable.strings */ = { + 241E0895B09C71AB423B2F9E /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( - AA0000000000000000000001 /* en */, - AA0000000000000000000002 /* zh-Hans */, - AA0000000000000000000005 /* zh-Hant */, + 9F04B811BB0012F6D2F07F91 /* en */, + 2641C6376DD6F5889F35510E /* zh-Hans */, + 02E502E2B5C6AECC76E5CFEF /* zh-Hant */, ); name = Localizable.strings; sourceTree = ""; @@ -250,6 +254,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "arm64 x86_64"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -285,7 +290,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = H3WXHVTP97; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -307,10 +312,10 @@ GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = PureMac/Info.plist; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; + ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.puremac.app; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -326,10 +331,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = PureMac/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_NAME = PureMac; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = YES; @@ -340,6 +342,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "arm64 x86_64"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -375,7 +378,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = H3WXHVTP97; ENABLE_NS_ASSERTIONS = NO; @@ -391,9 +394,10 @@ GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = PureMac/Info.plist; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.puremac.app; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -409,10 +413,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = PureMac/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_NAME = PureMac; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_128.png b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_128.png index 81631ee..8021ef6 100644 Binary files a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_128.png and b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_128.png differ diff --git a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png index 979ca42..f1a879a 100644 Binary files a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png and b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png differ diff --git a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_16.png b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_16.png index b25374b..7a8199e 100644 Binary files a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_16.png and b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_16.png differ diff --git a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png index 1db0c3b..ddafe2c 100644 Binary files a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png and b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png differ diff --git a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_256.png b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_256.png index 979ca42..f1a879a 100644 Binary files a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_256.png and b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_256.png differ diff --git a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png index ffce859..b7f5e0b 100644 Binary files a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png and b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png differ diff --git a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_32.png b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_32.png index 1db0c3b..ddafe2c 100644 Binary files a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_32.png and b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_32.png differ diff --git a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png index 047f6aa..357d048 100644 Binary files a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png and b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png differ diff --git a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_512.png b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_512.png index ffce859..b7f5e0b 100644 Binary files a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_512.png and b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_512.png differ diff --git a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png index 100eec3..f99dce0 100644 Binary files a/PureMac/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png and b/PureMac/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png differ diff --git a/PureMac/Assets.xcassets/SidebarLogo.imageset/Contents.json b/PureMac/Assets.xcassets/SidebarLogo.imageset/Contents.json new file mode 100644 index 0000000..ac2a76a --- /dev/null +++ b/PureMac/Assets.xcassets/SidebarLogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "logo_1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "logo_2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "logo_3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PureMac/Assets.xcassets/SidebarLogo.imageset/logo_1x.png b/PureMac/Assets.xcassets/SidebarLogo.imageset/logo_1x.png new file mode 100644 index 0000000..f95fa42 Binary files /dev/null and b/PureMac/Assets.xcassets/SidebarLogo.imageset/logo_1x.png differ diff --git a/PureMac/Assets.xcassets/SidebarLogo.imageset/logo_2x.png b/PureMac/Assets.xcassets/SidebarLogo.imageset/logo_2x.png new file mode 100644 index 0000000..19947f1 Binary files /dev/null and b/PureMac/Assets.xcassets/SidebarLogo.imageset/logo_2x.png differ diff --git a/PureMac/Assets.xcassets/SidebarLogo.imageset/logo_3x.png b/PureMac/Assets.xcassets/SidebarLogo.imageset/logo_3x.png new file mode 100644 index 0000000..ed339df Binary files /dev/null and b/PureMac/Assets.xcassets/SidebarLogo.imageset/logo_3x.png differ diff --git a/PureMac/Extensions/Theme.swift b/PureMac/Extensions/Theme.swift index 544589e..247729b 100644 --- a/PureMac/Extensions/Theme.swift +++ b/PureMac/Extensions/Theme.swift @@ -1,37 +1,71 @@ import SwiftUI +import AppKit + +// MARK: - NSColor Hex Support + +extension NSColor { + convenience init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 6: + (a, r, g, b) = (255, (int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF) + case 8: + (a, r, g, b) = ((int >> 24) & 0xFF, (int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (255, 0, 0, 0) + } + self.init( + srgbRed: CGFloat(r) / 255, + green: CGFloat(g) / 255, + blue: CGFloat(b) / 255, + alpha: CGFloat(a) / 255 + ) + } +} // MARK: - Color Theme extension Color { + // Adaptive color helper + private static func adaptive(light: String, dark: String) -> Color { + Color(nsColor: NSColor(name: nil) { appearance in + appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + ? NSColor(hex: dark) + : NSColor(hex: light) + }) + } + // Background colors - static let pmBackground = Color(hex: "0f0f1a") - static let pmSidebar = Color(hex: "161627") - static let pmCard = Color(hex: "1c1c35") - static let pmCardHover = Color(hex: "252545") + static let pmBackground = adaptive(light: "F5F5F7", dark: "1C1C1E") + static let pmSidebar = adaptive(light: "EEEEF0", dark: "2C2C2E") + static let pmCard = adaptive(light: "FFFFFF", dark: "3A3A3C") + static let pmCardHover = adaptive(light: "F0F0F2", dark: "48484A") // Accent colors - static let pmAccent = Color(hex: "6366f1") - static let pmAccentLight = Color(hex: "818cf8") - static let pmAccentDark = Color(hex: "4f46e5") + static let pmAccent = adaptive(light: "4F46E5", dark: "6366F1") + static let pmAccentLight = adaptive(light: "6366F1", dark: "818CF8") + static let pmAccentDark = adaptive(light: "3730A3", dark: "4F46E5") // Gradient colors - static let pmGradientStart = Color(hex: "6366f1") - static let pmGradientEnd = Color(hex: "a855f7") - static let pmGradientAlt = Color(hex: "ec4899") + static let pmGradientStart = adaptive(light: "4F46E5", dark: "6366F1") + static let pmGradientEnd = adaptive(light: "7C3AED", dark: "A855F7") // Status colors - static let pmSuccess = Color(hex: "10b981") - static let pmWarning = Color(hex: "f59e0b") - static let pmDanger = Color(hex: "ef4444") - static let pmInfo = Color(hex: "3b82f6") + static let pmSuccess = adaptive(light: "34C759", dark: "30D158") + static let pmWarning = adaptive(light: "FF9F0A", dark: "FF9F0A") + static let pmDanger = adaptive(light: "FF3B30", dark: "FF453A") + static let pmInfo = adaptive(light: "007AFF", dark: "0A84FF") // Text colors - static let pmTextPrimary = Color(hex: "f1f5f9") - static let pmTextSecondary = Color(hex: "94a3b8") - static let pmTextMuted = Color(hex: "64748b") + static let pmTextPrimary = adaptive(light: "1D1D1F", dark: "F5F5F7") + static let pmTextSecondary = adaptive(light: "6E6E73", dark: "98989D") + static let pmTextMuted = adaptive(light: "AEAEB2", dark: "636366") // Separator - static let pmSeparator = Color(hex: "2d2d50") + static let pmSeparator = adaptive(light: "D1D1D6", dark: "38383A") init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) @@ -72,31 +106,19 @@ struct AppGradients { ) static let danger = LinearGradient( - colors: [.pmDanger, Color(hex: "dc2626")], + colors: [.pmDanger, .pmDanger.opacity(0.85)], startPoint: .leading, endPoint: .trailing ) static let success = LinearGradient( - colors: [.pmSuccess, Color(hex: "059669")], + colors: [.pmSuccess, .pmSuccess.opacity(0.85)], startPoint: .leading, endPoint: .trailing ) - static let background = LinearGradient( - colors: [Color(hex: "0f0f1a"), Color(hex: "1a1a2e")], - startPoint: .top, - endPoint: .bottom - ) - - static let sidebar = LinearGradient( - colors: [Color(hex: "131325"), Color(hex: "1a1a35")], - startPoint: .top, - endPoint: .bottom - ) - static let scanRing = AngularGradient( - colors: [.pmGradientStart, .pmGradientEnd, .pmGradientAlt, .pmGradientStart], + colors: [.pmAccent, .pmAccentLight, .pmAccent], center: .center ) } @@ -105,11 +127,11 @@ struct AppGradients { extension View { func pmShadow(radius: CGFloat = 10, y: CGFloat = 4) -> some View { - self.shadow(color: .black.opacity(0.3), radius: radius, x: 0, y: y) + self.shadow(color: .black.opacity(0.15), radius: radius, x: 0, y: y) } - func pmGlow(color: Color = .pmAccent, radius: CGFloat = 20) -> some View { - self.shadow(color: color.opacity(0.3), radius: radius, x: 0, y: 0) + func pmGlow(color: Color = .pmAccent, radius: CGFloat = 8) -> some View { + self.shadow(color: color.opacity(0.15), radius: radius, x: 0, y: 0) } } diff --git a/PureMac/Info.plist b/PureMac/Info.plist index 2007cd2..e9d309a 100644 --- a/PureMac/Info.plist +++ b/PureMac/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.1 + 1.1.0 CFBundleVersion - 3 + 4 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) LSApplicationCategoryType diff --git a/PureMac/Models/Models.swift b/PureMac/Models/Models.swift index 78a9572..1f361b7 100644 --- a/PureMac/Models/Models.swift +++ b/PureMac/Models/Models.swift @@ -1,5 +1,31 @@ import SwiftUI +// MARK: - Appearance + +enum AppAppearance: String, CaseIterable, Identifiable, Codable { + case system = "system" + case light = "light" + case dark = "dark" + + var id: String { rawValue } + + var title: String { + switch self { + case .system: return String(localized: "System") + case .light: return String(localized: "Light") + case .dark: return String(localized: "Dark") + } + } + + var colorScheme: ColorScheme? { + switch self { + case .system: return nil + case .light: return .light + case .dark: return .dark + } + } +} + // MARK: - Cleaning Category enum CleaningCategory: String, CaseIterable, Identifiable, Codable { @@ -18,7 +44,7 @@ enum CleaningCategory: String, CaseIterable, Identifiable, Codable { var icon: String { switch self { - case .smartScan: return "sparkles" + case .smartScan: return "magnifyingglass" case .systemJunk: return "gearshape.fill" case .userCache: return "internaldrive.fill" case .aiApps: return "cpu.fill" @@ -28,6 +54,7 @@ enum CleaningCategory: String, CaseIterable, Identifiable, Codable { case .purgeableSpace: return "arrow.3.trianglepath" case .xcodeJunk: return "hammer.fill" case .brewCache: return "mug.fill" + } } @@ -43,6 +70,7 @@ enum CleaningCategory: String, CaseIterable, Identifiable, Codable { case .purgeableSpace: return "APFS purgeable disk space" case .xcodeJunk: return "Derived data, archives, and simulators" case .brewCache: return "Homebrew download cache" + } } @@ -58,6 +86,7 @@ enum CleaningCategory: String, CaseIterable, Identifiable, Codable { case .purgeableSpace: return .pmSuccess case .xcodeJunk: return Color(hex: "06b6d4") case .brewCache: return Color(hex: "84cc16") + } } diff --git a/PureMac/PureMacApp.swift b/PureMac/PureMacApp.swift index 0b3c5ee..3cb2744 100644 --- a/PureMac/PureMacApp.swift +++ b/PureMac/PureMacApp.swift @@ -3,13 +3,21 @@ import SwiftUI @main struct PureMacApp: App { @StateObject private var appViewModel = AppViewModel() + @AppStorage("PureMac.Appearance") private var appearance: AppAppearance = .system + @AppStorage("PureMac.OnboardingComplete") private var onboardingComplete = false var body: some Scene { WindowGroup { - ContentView() - .environmentObject(appViewModel) - .frame(minWidth: 900, minHeight: 600) - .preferredColorScheme(.dark) + Group { + if onboardingComplete { + ContentView() + } else { + OnboardingView(isComplete: $onboardingComplete) + } + } + .environmentObject(appViewModel) + .frame(minWidth: 900, minHeight: 600) + .preferredColorScheme(appearance.colorScheme) } .windowStyle(.hiddenTitleBar) .defaultSize(width: 1000, height: 680) diff --git a/PureMac/Views/CategoryDetailView.swift b/PureMac/Views/CategoryDetailView.swift index 92bf7dc..9db678e 100644 --- a/PureMac/Views/CategoryDetailView.swift +++ b/PureMac/Views/CategoryDetailView.swift @@ -199,7 +199,7 @@ struct CategoryDetailView: View { title: "Scan", icon: "magnifyingglass", gradient: LinearGradient( - colors: [category.color, category.color.opacity(0.7)], + colors: [category.color, category.color.opacity(0.8)], startPoint: .leading, endPoint: .trailing ) diff --git a/PureMac/Views/ContentView.swift b/PureMac/Views/ContentView.swift index e99f78f..4bc0c1b 100644 --- a/PureMac/Views/ContentView.swift +++ b/PureMac/Views/ContentView.swift @@ -45,6 +45,7 @@ struct ContentView: View { } } .background(Color.pmBackground) + .focusable(false) .onAppear { NSWindow.allowsAutomaticWindowTabbing = false } @@ -84,7 +85,7 @@ struct FullDiskAccessBanner: View { .foregroundColor(.white) .padding(.horizontal, 14) .padding(.vertical, 6) - .background(AppGradients.accent) + .background(Color.pmAccent) .cornerRadius(8) } .buttonStyle(.plain) @@ -140,9 +141,9 @@ struct TopBarView: View { RoundedRectangle(cornerRadius: 3) .fill( - vm.diskInfo.usedPercentage > 0.9 ? AppGradients.danger : - vm.diskInfo.usedPercentage > 0.7 ? AppGradients.accent : - AppGradients.success + vm.diskInfo.usedPercentage > 0.9 ? Color.pmDanger : + vm.diskInfo.usedPercentage > 0.7 ? Color.pmAccent : + Color.pmSuccess ) .frame(width: geo.size.width * vm.diskInfo.usedPercentage, height: 6) } diff --git a/PureMac/Views/OnboardingView.swift b/PureMac/Views/OnboardingView.swift new file mode 100644 index 0000000..657bc6c --- /dev/null +++ b/PureMac/Views/OnboardingView.swift @@ -0,0 +1,412 @@ +import SwiftUI + +struct OnboardingView: View { + @EnvironmentObject var vm: AppViewModel + @Binding var isComplete: Bool + + @State private var currentPage = 0 + @State private var fdaGranted = false + @State private var fdaCheckTimer: Timer? + @State private var logoScale: CGFloat = 0.5 + @State private var logoOpacity: Double = 0 + + var body: some View { + ZStack { + Color.pmBackground + .ignoresSafeArea() + + VStack(spacing: 0) { + // Page content + Group { + switch currentPage { + case 0: + welcomePage + case 1: + fdaPage + case 2: + readyPage + default: + EmptyView() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + // Bottom bar + bottomBar + .padding(.horizontal, 60) + .padding(.bottom, 40) + } + } + .frame(minWidth: 900, minHeight: 600) + .focusable(false) + } + + // MARK: - Welcome Page + + private var welcomePage: some View { + VStack(spacing: 24) { + Spacer() + + Image("SidebarLogo") + .resizable() + .interpolation(.high) + .frame(width: 96, height: 96) + .shadow(color: .black.opacity(0.1), radius: 12, y: 6) + .scaleEffect(logoScale) + .opacity(logoOpacity) + .onAppear { + withAnimation(.spring(response: 0.8, dampingFraction: 0.6)) { + logoScale = 1.0 + logoOpacity = 1.0 + } + } + + VStack(spacing: 8) { + Text("Welcome to PureMac") + .font(.system(size: 28, weight: .bold, design: .rounded)) + .foregroundColor(.pmTextPrimary) + + Text("Keep your Mac fast, clean, and optimized.") + .font(.system(size: 15, weight: .medium)) + .foregroundColor(.pmTextSecondary) + } + + // Feature highlights + HStack(spacing: 20) { + FeatureCard( + icon: "magnifyingglass", + color: .pmAccent, + title: "Smart Scan", + subtitle: "Find junk in seconds" + ) + FeatureCard( + icon: "trash.fill", + color: .pmDanger, + title: "One-Click Clean", + subtitle: "Remove files safely" + ) + FeatureCard( + icon: "xmark.app.fill", + color: Color(hex: "e11d48"), + title: "App Uninstaller", + subtitle: "Delete apps completely" + ) + } + .padding(.top, 20) + + Spacer() + } + } + + // MARK: - Full Disk Access Page + + private var fdaPage: some View { + VStack(spacing: 24) { + Spacer() + + ZStack { + Circle() + .fill(fdaGranted ? Color.pmSuccess.opacity(0.08) : Color.pmWarning.opacity(0.08)) + .frame(width: 120, height: 120) + + Image(systemName: fdaGranted ? "checkmark.shield.fill" : "lock.shield.fill") + .font(.system(size: 48, weight: .light)) + .foregroundColor(fdaGranted ? .pmSuccess : .pmWarning) + } + + VStack(spacing: 8) { + Text(fdaGranted ? LocalizedStringKey("Access Granted") : LocalizedStringKey("Full Disk Access")) + .font(.system(size: 28, weight: .bold, design: .rounded)) + .foregroundColor(.pmTextPrimary) + + Text(fdaGranted + ? LocalizedStringKey("PureMac can now scan all areas of your Mac.") + : LocalizedStringKey("PureMac needs Full Disk Access to scan Trash, Mail, Desktop, Documents, and Homebrew cache.")) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.pmTextSecondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + + if !fdaGranted { + VStack(spacing: 12) { + // Steps + VStack(alignment: .leading, spacing: 10) { + StepRow(number: 1, text: "Click \"Open System Settings\" below") + StepRow(number: 2, text: "Find PureMac in the list") + StepRow(number: 3, text: "Toggle the switch to enable access") + } + .padding(20) + .frame(maxWidth: 380) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color.pmCard.opacity(0.6)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.pmSeparator.opacity(0.4), lineWidth: 0.5) + ) + ) + + Button(action: { + vm.openFullDiskAccessSettings() + }) { + HStack(spacing: 8) { + Image(systemName: "gear") + .font(.system(size: 13, weight: .semibold)) + Text("Open System Settings") + .font(.system(size: 14, weight: .semibold, design: .rounded)) + } + .foregroundColor(.white) + .frame(height: 42) + .padding(.horizontal, 28) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(Color.pmWarning) + ) + .shadow(color: Color.pmWarning.opacity(0.2), radius: 6, y: 3) + } + .buttonStyle(.plain) + } + .padding(.top, 8) + } else { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 40)) + .foregroundColor(.pmSuccess) + .padding(.top, 8) + .transition(.scale.combined(with: .opacity)) + } + + Spacer() + } + .onAppear { startFDACheck() } + .onDisappear { fdaCheckTimer?.invalidate() } + } + + // MARK: - Ready Page + + private var readyPage: some View { + VStack(spacing: 24) { + Spacer() + + ZStack { + Circle() + .fill(Color.pmSuccess.opacity(0.06)) + .frame(width: 140, height: 140) + + Circle() + .stroke(Color.pmSuccess.opacity(0.15), lineWidth: 2) + .frame(width: 140, height: 140) + + Image("SidebarLogo") + .resizable() + .interpolation(.high) + .frame(width: 72, height: 72) + .shadow(color: .black.opacity(0.08), radius: 8, y: 4) + } + + VStack(spacing: 8) { + Text("You're All Set") + .font(.system(size: 28, weight: .bold, design: .rounded)) + .foregroundColor(.pmTextPrimary) + + Text("PureMac is ready to keep your Mac clean.") + .font(.system(size: 15, weight: .medium)) + .foregroundColor(.pmTextSecondary) + } + + // Summary + HStack(spacing: 20) { + StatusPill( + icon: fdaGranted ? "checkmark.circle.fill" : "exclamationmark.circle.fill", + text: fdaGranted ? "Full Disk Access" : "Limited Access", + color: fdaGranted ? .pmSuccess : .pmWarning + ) + } + .padding(.top, 8) + + Spacer() + } + } + + // MARK: - Bottom Bar + + private var bottomBar: some View { + HStack { + // Page dots + HStack(spacing: 8) { + ForEach(0..<3, id: \.self) { index in + Capsule() + .fill(currentPage == index ? Color.pmAccent : Color.pmSeparator) + .frame(width: currentPage == index ? 20 : 8, height: 8) + .animation(.pmSmooth, value: currentPage) + } + } + + Spacer() + + HStack(spacing: 12) { + // Skip button (pages 0-1) + if currentPage < 2 { + Button(action: { + withAnimation(.pmSmooth) { + currentPage = 2 + } + }) { + Text("Skip") + .font(.system(size: 13, weight: .medium, design: .rounded)) + .foregroundColor(.pmTextMuted) + } + .buttonStyle(.plain) + } + + // Next / Get Started button + Button(action: { + if currentPage < 2 { + withAnimation(.pmSmooth) { + currentPage += 1 + } + } else { + withAnimation(.pmSmooth) { + isComplete = true + } + } + }) { + HStack(spacing: 6) { + Text(currentPage == 2 ? LocalizedStringKey("Start Scanning") : LocalizedStringKey("Continue")) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + + if currentPage < 2 { + Image(systemName: "arrow.right") + .font(.system(size: 11, weight: .semibold)) + } + } + .foregroundColor(.white) + .frame(height: 38) + .padding(.horizontal, 24) + .background( + RoundedRectangle(cornerRadius: 9) + .fill(Color.pmAccent) + ) + .shadow(color: Color.pmAccent.opacity(0.15), radius: 4, y: 2) + } + .buttonStyle(.plain) + } + } + } + + // MARK: - FDA Check + + private func startFDACheck() { + fdaGranted = vm.hasFullDiskAccess + fdaCheckTimer?.invalidate() + fdaCheckTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + Task { @MainActor in + let granted = FullDiskAccessManager.shared.hasFullDiskAccess + if granted != fdaGranted { + withAnimation(.pmSmooth) { + fdaGranted = granted + vm.hasFullDiskAccess = granted + } + } + } + } + } +} + +// MARK: - Feature Card + +struct FeatureCard: View { + let icon: String + let color: Color + let title: LocalizedStringKey + let subtitle: LocalizedStringKey + + var body: some View { + VStack(spacing: 12) { + ZStack { + RoundedRectangle(cornerRadius: 14) + .fill(color.opacity(0.08)) + .frame(width: 48, height: 48) + + Image(systemName: icon) + .font(.system(size: 20)) + .foregroundColor(color) + } + + VStack(spacing: 4) { + Text(title) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .foregroundColor(.pmTextPrimary) + + Text(subtitle) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.pmTextMuted) + } + } + .frame(width: 160) + .padding(.vertical, 20) + .background( + RoundedRectangle(cornerRadius: 14) + .fill(Color.pmCard.opacity(0.5)) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(Color.pmSeparator.opacity(0.4), lineWidth: 0.5) + ) + ) + } +} + +// MARK: - Step Row + +struct StepRow: View { + let number: Int + let text: LocalizedStringKey + + var body: some View { + HStack(spacing: 12) { + ZStack { + Circle() + .fill(Color.pmAccent.opacity(0.1)) + .frame(width: 24, height: 24) + + Text("\(number)") + .font(.system(size: 11, weight: .bold, design: .rounded)) + .foregroundColor(.pmAccent) + } + + Text(text) + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.pmTextSecondary) + } + } +} + +// MARK: - Status Pill + +struct StatusPill: View { + let icon: String + let text: LocalizedStringKey + let color: Color + + var body: some View { + HStack(spacing: 6) { + Image(systemName: icon) + .font(.system(size: 12)) + .foregroundColor(color) + + Text(text) + .font(.system(size: 12, weight: .semibold, design: .rounded)) + .foregroundColor(.pmTextPrimary) + } + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background( + Capsule() + .fill(color.opacity(0.08)) + .overlay( + Capsule() + .stroke(color.opacity(0.15), lineWidth: 0.5) + ) + ) + } +} diff --git a/PureMac/Views/SettingsView.swift b/PureMac/Views/SettingsView.swift index 0c18bd7..98314cd 100644 --- a/PureMac/Views/SettingsView.swift +++ b/PureMac/Views/SettingsView.swift @@ -2,18 +2,19 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var vm: AppViewModel + @AppStorage("PureMac.Appearance") private var appearance: AppAppearance = .system var body: some View { TabView { - ScheduleSettingsTab() - .environmentObject(vm) + GeneralSettingsTab() .tabItem { - Label("Schedule", systemImage: "clock.fill") + Label("General", systemImage: "gearshape.fill") } - GeneralSettingsTab() + ScheduleSettingsTab() + .environmentObject(vm) .tabItem { - Label("General", systemImage: "gearshape.fill") + Label("Schedule", systemImage: "clock.fill") } AboutTab() @@ -21,7 +22,52 @@ struct SettingsView: View { Label("About", systemImage: "info.circle.fill") } } - .frame(width: 520, height: 440) + .frame(width: 520, height: 480) + .preferredColorScheme(appearance.colorScheme) + .focusable(false) + } +} + +// MARK: - General Settings + +struct GeneralSettingsTab: View { + @AppStorage("PureMac.LaunchAtLogin") private var launchAtLogin = false + @AppStorage("PureMac.ShowInDock") private var showInDock = true + @AppStorage("PureMac.ShowMenuBarIcon") private var showMenuBarIcon = true + @AppStorage("PureMac.Appearance") private var appearance: AppAppearance = .system + + var body: some View { + Form { + Section { + Picker("Appearance", selection: $appearance) { + ForEach(AppAppearance.allCases) { option in + Text(option.title).tag(option) + } + } + .pickerStyle(.segmented) + } header: { + Label("Appearance", systemImage: "paintbrush.fill") + } + + Section { + Toggle("Launch at login", isOn: $launchAtLogin) + Toggle("Show in Dock", isOn: $showInDock) + Toggle("Show menu bar icon", isOn: $showMenuBarIcon) + } header: { + Label("App Behavior", systemImage: "switch.2") + } + + Section { + Text("PureMac will never delete system-critical files. Only caches, logs, temporary files, and user-selected items are removed.") + .font(.system(size: 12)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } header: { + Label("Safety", systemImage: "shield.checkered") + } + } + .formStyle(.grouped) + .scrollContentBackground(.hidden) } } @@ -31,32 +77,24 @@ struct ScheduleSettingsTab: View { @EnvironmentObject var vm: AppViewModel var body: some View { - VStack(alignment: .leading, spacing: 24) { - // Enable toggle - VStack(alignment: .leading, spacing: 8) { + Form { + Section { Toggle(isOn: Binding( get: { vm.scheduler.config.isEnabled }, set: { vm.scheduler.toggleEnabled($0) } )) { - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: 2) { Text("Automatic Cleaning") - .font(.system(size: 14, weight: .semibold)) + .font(.system(size: 13, weight: .medium)) Text("Automatically scan and clean your Mac on a schedule") - .font(.system(size: 12)) + .font(.system(size: 11)) .foregroundColor(.secondary) } } - .toggleStyle(.switch) } - Divider() - - // Interval picker - VStack(alignment: .leading, spacing: 8) { - Text("Scan Interval") - .font(.system(size: 13, weight: .semibold)) - - Picker("", selection: Binding( + Section { + Picker("Scan Interval", selection: Binding( get: { vm.scheduler.config.interval }, set: { vm.scheduler.updateSchedule(interval: $0) } )) { @@ -64,62 +102,40 @@ struct ScheduleSettingsTab: View { Text(LocalizedStringKey(interval.rawValue)).tag(interval) } } - .pickerStyle(.menu) - .frame(width: 200) .disabled(!vm.scheduler.config.isEnabled) + } header: { + Label("Schedule", systemImage: "calendar.badge.clock") } - Divider() - - // Auto-clean options - VStack(alignment: .leading, spacing: 12) { - Text("Automation") - .font(.system(size: 13, weight: .semibold)) - + Section { Toggle("Auto-clean after scan", isOn: $vm.scheduler.config.autoClean) - .toggleStyle(.switch) .disabled(!vm.scheduler.config.isEnabled) if vm.scheduler.config.autoClean { - HStack { - Text("Minimum junk size to trigger clean:") - .font(.system(size: 12)) - .foregroundColor(.secondary) - - Picker("", selection: Binding( - get: { vm.scheduler.config.minimumCleanSize }, - set: { vm.scheduler.config.minimumCleanSize = $0 } - )) { - Text("50 MB").tag(Int64(50 * 1024 * 1024)) - Text("100 MB").tag(Int64(100 * 1024 * 1024)) - Text("250 MB").tag(Int64(250 * 1024 * 1024)) - Text("500 MB").tag(Int64(500 * 1024 * 1024)) - Text("1 GB").tag(Int64(1024 * 1024 * 1024)) - } - .pickerStyle(.menu) - .frame(width: 120) + Picker("Minimum junk size", selection: Binding( + get: { vm.scheduler.config.minimumCleanSize }, + set: { vm.scheduler.config.minimumCleanSize = $0 } + )) { + Text("50 MB").tag(Int64(50 * 1024 * 1024)) + Text("100 MB").tag(Int64(100 * 1024 * 1024)) + Text("250 MB").tag(Int64(250 * 1024 * 1024)) + Text("500 MB").tag(Int64(500 * 1024 * 1024)) + Text("1 GB").tag(Int64(1024 * 1024 * 1024)) } - .padding(.leading, 20) } Toggle("Auto-purge purgeable space", isOn: $vm.scheduler.config.autoPurge) - .toggleStyle(.switch) .disabled(!vm.scheduler.config.isEnabled) Toggle("Show notification on completion", isOn: $vm.scheduler.config.notifyOnCompletion) - .toggleStyle(.switch) .disabled(!vm.scheduler.config.isEnabled) + } header: { + Label("Automation", systemImage: "bolt.fill") } - Divider() - - // Status - VStack(alignment: .leading, spacing: 8) { - Text("Status") - .font(.system(size: 13, weight: .semibold)) - - HStack(spacing: 16) { - VStack(alignment: .leading, spacing: 4) { + Section { + HStack { + VStack(alignment: .leading, spacing: 2) { Text("Last run") .font(.system(size: 11)) .foregroundColor(.secondary) @@ -127,7 +143,9 @@ struct ScheduleSettingsTab: View { .font(.system(size: 12, weight: .medium)) } - VStack(alignment: .leading, spacing: 4) { + Spacer() + + VStack(alignment: .trailing, spacing: 2) { Text("Next run") .font(.system(size: 11)) .foregroundColor(.secondary) @@ -135,52 +153,12 @@ struct ScheduleSettingsTab: View { .font(.system(size: 12, weight: .medium)) } } + } header: { + Label("Status", systemImage: "chart.bar.fill") } - - Spacer() } - .padding(24) - } -} - -// MARK: - General Settings - -struct GeneralSettingsTab: View { - @AppStorage("PureMac.LaunchAtLogin") private var launchAtLogin = false - @AppStorage("PureMac.ShowInDock") private var showInDock = true - @AppStorage("PureMac.ShowMenuBarIcon") private var showMenuBarIcon = true - - var body: some View { - VStack(alignment: .leading, spacing: 24) { - VStack(alignment: .leading, spacing: 12) { - Text("App Behavior") - .font(.system(size: 13, weight: .semibold)) - - Toggle("Launch at login", isOn: $launchAtLogin) - .toggleStyle(.switch) - - Toggle("Show in Dock", isOn: $showInDock) - .toggleStyle(.switch) - - Toggle("Show menu bar icon", isOn: $showMenuBarIcon) - .toggleStyle(.switch) - } - - Divider() - - VStack(alignment: .leading, spacing: 12) { - Text("Safety") - .font(.system(size: 13, weight: .semibold)) - - Text("PureMac will never delete system-critical files. Only caches, logs, temporary files, and user-selected items are removed.") - .font(.system(size: 12)) - .foregroundColor(.secondary) - .fixedSize(horizontal: false, vertical: true) - } - - Spacer() - } - .padding(24) + .formStyle(.grouped) + .scrollContentBackground(.hidden) } } @@ -188,44 +166,51 @@ struct GeneralSettingsTab: View { struct AboutTab: View { var body: some View { - VStack(spacing: 20) { + VStack(spacing: 0) { Spacer() - ZStack { - Circle() - .fill( - LinearGradient( - colors: [Color(hex: "6366f1"), Color(hex: "a855f7")], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .frame(width: 80, height: 80) - - Image(systemName: "sparkles") - .font(.system(size: 32, weight: .bold)) - .foregroundColor(.white) - } + // App icon + Image("SidebarLogo") + .resizable() + .interpolation(.high) + .frame(width: 80, height: 80) + .shadow(color: .black.opacity(0.08), radius: 8, y: 4) + + Spacer().frame(height: 16) Text("PureMac") - .font(.system(size: 24, weight: .bold, design: .rounded)) + .font(.system(size: 22, weight: .bold, design: .rounded)) + + Spacer().frame(height: 4) Text("Version \(AppConstants.appVersion)") - .font(.system(size: 13)) + .font(.system(size: 12)) .foregroundColor(.secondary) + Spacer().frame(height: 16) + Text("A free, open-source Mac cleaning utility.\nKeep your Mac fast, clean, and optimized.") .font(.system(size: 12)) .foregroundColor(.secondary) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) - Link("GitHub Repository", destination: URL(string: "https://github.com/momenbasel/PureMac")!) - .font(.system(size: 12)) + Spacer().frame(height: 20) - Text("MIT License") - .font(.system(size: 11)) - .foregroundColor(.secondary) + HStack(spacing: 16) { + Link(destination: URL(string: "https://github.com/momenbasel/PureMac")!) { + HStack(spacing: 5) { + Image(systemName: "link") + .font(.system(size: 10)) + Text("GitHub Repository") + .font(.system(size: 12)) + } + } + + Text("MIT License") + .font(.system(size: 11)) + .foregroundColor(.secondary) + } Spacer() } diff --git a/PureMac/Views/SidebarView.swift b/PureMac/Views/SidebarView.swift index 3e79910..7da4b89 100644 --- a/PureMac/Views/SidebarView.swift +++ b/PureMac/Views/SidebarView.swift @@ -5,55 +5,43 @@ struct SidebarView: View { var body: some View { VStack(spacing: 0) { - // App logo / title - VStack(spacing: 8) { - HStack(spacing: 10) { - ZStack { - Circle() - .fill(AppGradients.primary) - .frame(width: 36, height: 36) - Image(systemName: "sparkles") - .font(.system(size: 16, weight: .bold)) - .foregroundColor(.white) - } - + // App branding + HStack(spacing: 12) { + Image("SidebarLogo") + .resizable() + .interpolation(.high) + .frame(width: 36, height: 36) + .shadow(color: .black.opacity(0.1), radius: 4, y: 2) + + VStack(alignment: .leading, spacing: 1) { Text("PureMac") - .font(.system(size: 20, weight: .bold, design: .rounded)) + .font(.system(size: 16, weight: .bold, design: .rounded)) .foregroundColor(.pmTextPrimary) - Spacer() + Text("System Cleaner") + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.pmTextMuted) } - .padding(.horizontal, 20) + + Spacer() } - .padding(.top, 48) // Account for title bar + .padding(.horizontal, 20) + .padding(.top, 48) .padding(.bottom, 24) + // Smart Scan card + SmartScanSidebarCard( + isSelected: vm.selectedCategory == .smartScan, + totalJunk: vm.totalJunkSize + ) + .onTapGesture { vm.selectedCategory = .smartScan } + .padding(.horizontal, 12) + .padding(.bottom, 16) + // Category list ScrollView(.vertical, showsIndicators: false) { - VStack(spacing: 2) { - // Smart Scan - always first - SidebarItem( - category: .smartScan, - isSelected: vm.selectedCategory == .smartScan, - resultSize: vm.totalJunkSize - ) - .onTapGesture { vm.selectedCategory = .smartScan } - - Divider() - .background(Color.pmSeparator) - .padding(.horizontal, 16) - .padding(.vertical, 8) - - // Section header - HStack { - Text("CLEANING") - .font(.system(size: 10, weight: .bold, design: .rounded)) - .foregroundColor(.pmTextMuted) - .tracking(1.2) - Spacer() - } - .padding(.horizontal, 20) - .padding(.bottom, 4) + VStack(spacing: 0) { + SectionHeader(title: "CLEANING") ForEach(CleaningCategory.scannable) { category in SidebarItem( @@ -63,53 +51,36 @@ struct SidebarView: View { ) .onTapGesture { vm.selectedCategory = category } } + } .padding(.bottom, 16) } Spacer() - // Bottom info + // Bottom status VStack(spacing: 8) { - Divider() - .background(Color.pmSeparator) - if let lastCleaned = vm.lastCleanedDate { - HStack { - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 10)) - .foregroundColor(.pmSuccess) - Text("Last cleaned: \(timeAgo(lastCleaned))") - .font(.system(size: 10)) + HStack(spacing: 6) { + Circle() + .fill(Color.pmSuccess) + .frame(width: 6, height: 6) + + Text("Cleaned \(timeAgo(lastCleaned))") + .font(.system(size: 10, weight: .medium)) .foregroundColor(.pmTextMuted) } - .padding(.horizontal, 16) } Text("v\(AppConstants.appVersion)") - .font(.system(size: 10)) - .foregroundColor(.pmTextMuted) - .padding(.bottom, 12) + .font(.system(size: 9, weight: .medium)) + .foregroundColor(.pmTextMuted.opacity(0.6)) } + .padding(.bottom, 14) } .background( - ZStack { - AppGradients.sidebar - .ignoresSafeArea() - // Subtle right border glow - HStack { - Spacer() - Rectangle() - .fill( - LinearGradient( - colors: [.pmAccent.opacity(0.05), .clear], - startPoint: .trailing, - endPoint: .leading - ) - ) - .frame(width: 1) - } - } + Color.pmSidebar + .ignoresSafeArea() ) } @@ -120,6 +91,84 @@ struct SidebarView: View { } } +// MARK: - Smart Scan Card + +struct SmartScanSidebarCard: View { + let isSelected: Bool + let totalJunk: Int64 + + @State private var isHovering = false + + var body: some View { + HStack(spacing: 12) { + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(Color.pmAccent) + .frame(width: 36, height: 36) + + Image(systemName: "magnifyingglass") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Smart Scan") + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .foregroundColor(.pmTextPrimary) + + if totalJunk > 0 { + Text(ByteCountFormatter.string(fromByteCount: totalJunk, countStyle: .file) + " found") + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.pmAccentLight) + } else { + Text("Scan everything at once") + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.pmTextMuted) + } + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 10, weight: .semibold)) + .foregroundColor(.pmTextMuted) + } + .padding(.horizontal, 14) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(isSelected ? Color.pmAccent.opacity(0.12) : Color.pmCard.opacity(isHovering ? 0.8 : 0.5)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(isSelected ? Color.pmAccent.opacity(0.3) : Color.clear, lineWidth: 1) + ) + ) + .onHover { h in + withAnimation(.pmSmooth) { isHovering = h } + } + .contentShape(Rectangle()) + } +} + +// MARK: - Section Header + +struct SectionHeader: View { + let title: String + + var body: some View { + HStack { + Text(title) + .font(.system(size: 10, weight: .bold, design: .rounded)) + .foregroundColor(.pmTextMuted) + .tracking(1.2) + Spacer() + } + .padding(.horizontal, 20) + .padding(.bottom, 6) + .padding(.top, 2) + } +} + // MARK: - Sidebar Item struct SidebarItem: View { @@ -130,48 +179,55 @@ struct SidebarItem: View { @State private var isHovering = false var body: some View { - HStack(spacing: 12) { - // Icon + HStack(spacing: 0) { + // Selection indicator bar + RoundedRectangle(cornerRadius: 2) + .fill(isSelected ? category.color : Color.clear) + .frame(width: 3, height: 20) + .padding(.trailing, 9) + + // Icon with colored background ZStack { RoundedRectangle(cornerRadius: 8) - .fill(isSelected ? category.color.opacity(0.2) : Color.clear) - .frame(width: 32, height: 32) + .fill(category.color.opacity(isSelected ? 0.15 : 0.08)) + .frame(width: 30, height: 30) Image(systemName: category.icon) - .font(.system(size: 14)) - .foregroundColor(isSelected ? category.color : .pmTextSecondary) + .font(.system(size: 13)) + .foregroundColor(isSelected ? category.color : category.color.opacity(0.6)) } // Label - VStack(alignment: .leading, spacing: 2) { + VStack(alignment: .leading, spacing: 1) { Text(LocalizedStringKey(category.rawValue)) - .font(.pmBody) + .font(.system(size: 12, weight: isSelected ? .semibold : .regular, design: .rounded)) .foregroundColor(isSelected ? .pmTextPrimary : .pmTextSecondary) .lineLimit(1) - - if let size = resultSize, size > 0 { - Text(ByteCountFormatter.string(fromByteCount: size, countStyle: .file)) - .font(.system(size: 10, weight: .medium, design: .rounded)) - .foregroundColor(category.color) - } } + .padding(.leading, 10) Spacer() // Size badge - if let size = resultSize, size > 0, !isSelected { - Circle() - .fill(category.color.opacity(0.2)) - .frame(width: 8, height: 8) + if let size = resultSize, size > 0 { + Text(ByteCountFormatter.string(fromByteCount: size, countStyle: .file)) + .font(.system(size: 9, weight: .semibold, design: .rounded)) + .foregroundColor(isSelected ? category.color : .pmTextMuted) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background( + Capsule() + .fill(category.color.opacity(isSelected ? 0.12 : 0.06)) + ) } } - .padding(.horizontal, 16) - .padding(.vertical, 8) + .padding(.trailing, 14) + .padding(.vertical, 6) .background( - RoundedRectangle(cornerRadius: 10) - .fill(isSelected ? Color.pmCard : (isHovering ? Color.pmCard.opacity(0.5) : .clear)) + RoundedRectangle(cornerRadius: 8) + .fill(isSelected ? Color.pmCard.opacity(0.6) : (isHovering ? Color.pmCard.opacity(0.3) : .clear)) + .padding(.horizontal, 6) ) - .padding(.horizontal, 8) .onHover { hovering in withAnimation(.pmSmooth) { isHovering = hovering } } diff --git a/PureMac/Views/SmartScanView.swift b/PureMac/Views/SmartScanView.swift index f5c6204..3f56eef 100644 --- a/PureMac/Views/SmartScanView.swift +++ b/PureMac/Views/SmartScanView.swift @@ -32,46 +32,50 @@ struct SmartScanView: View { // MARK: - Idle View private var idleView: some View { - VStack(spacing: 24) { + VStack(spacing: 32) { + // Logo-centered circle ZStack { - // Outer glow ring + // Outer decorative ring Circle() - .stroke(Color.pmAccent.opacity(0.1), lineWidth: 2) - .frame(width: 220, height: 220) + .stroke(Color.pmSeparator.opacity(0.3), style: StrokeStyle(lineWidth: 1, dash: [6, 4])) + .frame(width: 240, height: 240) + // Main ring track Circle() - .stroke(Color.pmAccent.opacity(0.05), lineWidth: 1) - .frame(width: 250, height: 250) + .stroke(Color.pmSeparator.opacity(0.2), lineWidth: 8) + .frame(width: 200, height: 200) - // Main circle + // Accent arc hint Circle() - .fill( - RadialGradient( - colors: [Color.pmAccent.opacity(0.15), Color.pmBackground], - center: .center, - startRadius: 20, - endRadius: 110 - ) - ) + .trim(from: 0, to: 0.3) + .stroke(Color.pmAccent.opacity(0.15), style: StrokeStyle(lineWidth: 8, lineCap: .round)) .frame(width: 200, height: 200) + .rotationEffect(.degrees(-90)) - VStack(spacing: 8) { - Image(systemName: "sparkles") - .font(.system(size: 40)) - .foregroundColor(.pmAccentLight) + // Inner fill + Circle() + .fill(Color.pmAccent.opacity(0.04)) + .frame(width: 188, height: 188) + + // App logo as centerpiece + VStack(spacing: 12) { + Image("SidebarLogo") + .resizable() + .interpolation(.high) + .frame(width: 56, height: 56) + .shadow(color: .black.opacity(0.08), radius: 6, y: 3) Text("Smart Scan") - .font(.pmHeadline) + .font(.system(size: 16, weight: .semibold, design: .rounded)) .foregroundColor(.pmTextPrimary) - Text("Click Scan to start") - .font(.pmCaption) + Text("Analyze all categories") + .font(.system(size: 11, weight: .medium)) .foregroundColor(.pmTextMuted) } } - .pmGlow(color: .pmAccent, radius: 40) - // Disk overview + // Disk overview cards diskOverview } } @@ -79,50 +83,65 @@ struct SmartScanView: View { // MARK: - Scanning View private var scanningView: some View { - VStack(spacing: 24) { + ZStack { + // Circle — pinned above center, never moves ZStack { - // Background ring + // Thick track Circle() - .stroke(Color.pmCard, lineWidth: 8) + .stroke(Color.pmSeparator.opacity(0.15), lineWidth: 10) .frame(width: 200, height: 200) // Progress ring Circle() .trim(from: 0, to: vm.scanProgress) .stroke( - AppGradients.scanRing, - style: StrokeStyle(lineWidth: 8, lineCap: .round) + AngularGradient( + colors: [.pmAccent, .pmAccentLight, .pmAccent], + center: .center + ), + style: StrokeStyle(lineWidth: 10, lineCap: .round) ) .frame(width: 200, height: 200) .rotationEffect(.degrees(-90)) .animation(.easeInOut(duration: 0.3), value: vm.scanProgress) - // Spinning outer ring + // Spinning outer indicator Circle() - .trim(from: 0, to: 0.3) + .trim(from: 0, to: 0.2) .stroke( - Color.pmAccent.opacity(0.3), - style: StrokeStyle(lineWidth: 2, lineCap: .round) + Color.pmAccent.opacity(0.15), + style: StrokeStyle(lineWidth: 1.5, lineCap: .round) ) - .frame(width: 230, height: 230) + .frame(width: 226, height: 226) .rotationEffect(.degrees(rotationAngle)) - VStack(spacing: 4) { - Text("\(Int(vm.scanProgress * 100))%") - .font(.pmLargeNumber) - .foregroundColor(.pmTextPrimary) - .contentTransition(.numericText()) + // Center content + VStack(spacing: 2) { + HStack(alignment: .firstTextBaseline, spacing: 1) { + Text("\(Int(vm.scanProgress * 100))") + .font(.system(size: 48, weight: .bold, design: .rounded)) + .foregroundColor(.pmTextPrimary) + .contentTransition(.numericText()) + + Text("%") + .font(.system(size: 20, weight: .medium, design: .rounded)) + .foregroundColor(.pmTextMuted) + } Text(LocalizedStringKey(vm.currentScanCategory)) - .font(.pmCaption) + .font(.system(size: 11, weight: .medium)) .foregroundColor(.pmTextSecondary) .lineLimit(1) + .padding(.top, 4) } } + .offset(y: -80) - // Live results + // Results — pinned below center, staggered fade-in if !vm.allResults.isEmpty { liveResults + .offset(y: 140) + .transition(.opacity) } } .onAppear { startRotation() } @@ -133,43 +152,47 @@ struct SmartScanView: View { private var completedView: some View { VStack(spacing: 24) { ZStack { + // Track Circle() - .stroke(Color.pmCard, lineWidth: 8) + .stroke(Color.pmSeparator.opacity(0.15), lineWidth: 10) .frame(width: 200, height: 200) + // Full ring Circle() - .trim(from: 0, to: 1) - .stroke( - AppGradients.primary, - style: StrokeStyle(lineWidth: 8, lineCap: .round) - ) + .stroke(Color.pmAccent, style: StrokeStyle(lineWidth: 10, lineCap: .round)) .frame(width: 200, height: 200) - .rotationEffect(.degrees(-90)) + // Center content VStack(spacing: 4) { - Text(ByteCountFormatter.string(fromByteCount: vm.totalJunkSize, countStyle: .file)) - .font(.pmLargeNumber) - .foregroundColor(.pmTextPrimary) + if vm.totalJunkSize > 0 { + Text(ByteCountFormatter.string(fromByteCount: vm.totalJunkSize, countStyle: .file)) + .font(.system(size: 34, weight: .bold, design: .rounded)) + .foregroundColor(.pmTextPrimary) - Text("junk found") - .font(.pmSubheadline) - .foregroundColor(.pmTextSecondary) + Text("junk found") + .font(.system(size: 13, weight: .medium)) + .foregroundColor(.pmTextSecondary) + } else { + Image(systemName: "checkmark") + .font(.system(size: 36, weight: .light)) + .foregroundColor(.pmSuccess) + + Text("All clean") + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundColor(.pmTextPrimary) + } } } - .pmGlow(color: vm.totalJunkSize > 0 ? .pmWarning : .pmSuccess, radius: 30) + + // Segmented breakdown bar + if !vm.allResults.isEmpty { + junkBreakdownBar + .padding(.top, 4) + } // Results breakdown if !vm.allResults.isEmpty { resultsBreakdown - } else { - VStack(spacing: 8) { - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 32)) - .foregroundColor(.pmSuccess) - Text("Your Mac is clean!") - .font(.pmSubheadline) - .foregroundColor(.pmTextSecondary) - } } } } @@ -180,32 +203,29 @@ struct SmartScanView: View { VStack(spacing: 24) { ZStack { Circle() - .stroke(Color.pmCard, lineWidth: 8) + .stroke(Color.pmSeparator.opacity(0.15), lineWidth: 10) .frame(width: 200, height: 200) Circle() .trim(from: 0, to: vm.cleanProgress) - .stroke( - AppGradients.danger, - style: StrokeStyle(lineWidth: 8, lineCap: .round) - ) + .stroke(Color.pmDanger, style: StrokeStyle(lineWidth: 10, lineCap: .round)) .frame(width: 200, height: 200) .rotationEffect(.degrees(-90)) .animation(.easeInOut(duration: 0.3), value: vm.cleanProgress) VStack(spacing: 4) { Image(systemName: "trash.fill") - .font(.system(size: 28)) + .font(.system(size: 22, weight: .light)) .foregroundColor(.pmDanger) - Text("Cleaning...") - .font(.pmSubheadline) - .foregroundColor(.pmTextSecondary) - Text("\(Int(vm.cleanProgress * 100))%") - .font(.pmMediumNumber) + .font(.system(size: 32, weight: .bold, design: .rounded)) .foregroundColor(.pmTextPrimary) .contentTransition(.numericText()) + + Text("Cleaning...") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.pmTextSecondary) } } } @@ -214,95 +234,163 @@ struct SmartScanView: View { // MARK: - Cleaned View private var cleanedView: some View { - VStack(spacing: 24) { + VStack(spacing: 16) { ZStack { Circle() - .fill(Color.pmSuccess.opacity(0.1)) + .stroke(Color.pmSuccess.opacity(0.2), lineWidth: 10) .frame(width: 200, height: 200) + Circle() + .fill(Color.pmSuccess.opacity(0.04)) + .frame(width: 190, height: 190) + VStack(spacing: 8) { - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 48)) + Image(systemName: "checkmark") + .font(.system(size: 32, weight: .light)) .foregroundColor(.pmSuccess) Text(ByteCountFormatter.string(fromByteCount: vm.totalFreedSpace, countStyle: .file)) - .font(.pmLargeNumber) + .font(.system(size: 30, weight: .bold, design: .rounded)) .foregroundColor(.pmSuccess) Text("freed up") - .font(.pmSubheadline) + .font(.system(size: 13, weight: .medium)) .foregroundColor(.pmTextSecondary) } } - .pmGlow(color: .pmSuccess, radius: 40) } .transition(.scale.combined(with: .opacity)) } - // MARK: - Sub Views + // MARK: - Junk Breakdown Bar - private var diskOverview: some View { - HStack(spacing: 24) { - DiskStatCard( - title: "Total", - value: vm.diskInfo.formattedTotal, - icon: "internaldrive.fill", - color: .pmAccent - ) - DiskStatCard( - title: "Used", - value: vm.diskInfo.formattedUsed, - icon: "chart.pie.fill", - color: .pmWarning - ) - DiskStatCard( - title: "Free", - value: vm.diskInfo.formattedFree, - icon: "checkmark.circle.fill", - color: .pmSuccess + private var junkBreakdownBar: some View { + VStack(spacing: 8) { + GeometryReader { geo in + HStack(spacing: 1.5) { + ForEach(vm.allResults) { result in + let fraction = vm.totalJunkSize > 0 + ? CGFloat(result.totalSize) / CGFloat(vm.totalJunkSize) + : 0 + + RoundedRectangle(cornerRadius: 3) + .fill(result.category.color) + .frame(width: max(4, geo.size.width * fraction)) + } + } + } + .frame(height: 8) + .background( + RoundedRectangle(cornerRadius: 3) + .fill(Color.pmSeparator.opacity(0.2)) ) + .clipShape(RoundedRectangle(cornerRadius: 3)) + + // Legend + HStack(spacing: 12) { + ForEach(vm.allResults.prefix(5)) { result in + HStack(spacing: 4) { + Circle() + .fill(result.category.color) + .frame(width: 6, height: 6) + + Text(LocalizedStringKey(result.category.rawValue)) + .font(.system(size: 9, weight: .medium)) + .foregroundColor(.pmTextMuted) + .lineLimit(1) + } + } + + if vm.allResults.count > 5 { + Text("+\(vm.allResults.count - 5) more") + .font(.system(size: 9, weight: .medium)) + .foregroundColor(.pmTextMuted) + } + } + } + .frame(maxWidth: 480) + } + + // MARK: - Disk Overview + + private var diskOverview: some View { + HStack(spacing: 16) { + DiskStatCard(title: "Total", value: vm.diskInfo.formattedTotal, icon: "internaldrive.fill", color: .pmAccent) + DiskStatCard(title: "Used", value: vm.diskInfo.formattedUsed, icon: "chart.pie.fill", color: .pmWarning) + DiskStatCard(title: "Free", value: vm.diskInfo.formattedFree, icon: "checkmark.circle.fill", color: .pmSuccess) if vm.diskInfo.purgeableSpace > 0 { - DiskStatCard( - title: "Purgeable", - value: vm.diskInfo.formattedPurgeable, - icon: "arrow.3.trianglepath", - color: .pmInfo - ) + DiskStatCard(title: "Purgeable", value: vm.diskInfo.formattedPurgeable, icon: "arrow.3.trianglepath", color: .pmInfo) } } } + // MARK: - Live Results + private var liveResults: some View { - VStack(spacing: 8) { - ForEach(vm.allResults.prefix(6)) { result in - HStack(spacing: 12) { - Image(systemName: result.category.icon) - .font(.system(size: 12)) - .foregroundColor(result.category.color) - .frame(width: 20) + VStack(spacing: 2) { + ForEach(Array(vm.allResults.prefix(6).enumerated()), id: \.element.id) { index, result in + HStack(spacing: 10) { + ZStack { + RoundedRectangle(cornerRadius: 5) + .fill(result.category.color.opacity(0.1)) + .frame(width: 22, height: 22) + + Image(systemName: result.category.icon) + .font(.system(size: 10)) + .foregroundColor(result.category.color) + } Text(LocalizedStringKey(result.category.rawValue)) - .font(.pmCaption) + .font(.system(size: 11, weight: .medium)) .foregroundColor(.pmTextSecondary) Spacer() Text(result.formattedSize) - .font(.pmCaption) + .font(.system(size: 11, weight: .semibold, design: .rounded)) .foregroundColor(.pmTextPrimary) } - .padding(.horizontal, 16) - .padding(.vertical, 6) + .padding(.horizontal, 14) + .padding(.vertical, 5) + .opacity(appearingResults.contains(result.id) ? 1 : 0) + .offset(y: appearingResults.contains(result.id) ? 0 : 6) + .animation(.easeOut(duration: 0.3).delay(Double(index) * 0.05), value: appearingResults) } } - .padding(.vertical, 12) - .background(Color.pmCard.opacity(0.5)) - .cornerRadius(12) - .frame(maxWidth: 400) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(Color.pmCard.opacity(0.5)) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.pmSeparator.opacity(0.5), lineWidth: 0.5) + ) + ) + .frame(maxWidth: 380) + .onChange(of: vm.allResults.count) { _ in + updateAppearingResults() + } + .onAppear { + updateAppearingResults() + } } + @State private var appearingResults: Set = [] + + private func updateAppearingResults() { + for result in vm.allResults.prefix(6) { + if !appearingResults.contains(result.id) { + withAnimation { + appearingResults.insert(result.id) + } + } + } + } + + // MARK: - Results Breakdown + private var resultsBreakdown: some View { - VStack(spacing: 8) { + VStack(spacing: 6) { ForEach(vm.allResults) { result in ResultRow(result: result) { withAnimation(.pmSpring) { @@ -311,108 +399,47 @@ struct SmartScanView: View { } } } - .frame(maxWidth: 450) + .frame(maxWidth: 480) } // MARK: - Action Bar private var actionBar: some View { - HStack(spacing: 16) { + HStack(spacing: 12) { switch vm.scanState { case .idle: - GradientActionButton( - title: "Scan", - icon: "magnifyingglass", - gradient: AppGradients.primary - ) { - withAnimation(.pmSpring) { - vm.startSmartScan() - } + ActionButton(title: "Scan", icon: "magnifyingglass", color: .pmAccent) { + withAnimation(.pmSpring) { vm.startSmartScan() } } case .scanning: - Button(action: {}) { - HStack(spacing: 8) { - ProgressView() - .progressViewStyle(.circular) - .scaleEffect(0.7) - .tint(.white) - Text("Scanning...") - .font(.pmSubheadline) - .foregroundColor(.pmTextSecondary) - } - .frame(width: 200, height: 44) - .background(Color.pmCard) - .cornerRadius(12) - } - .buttonStyle(.plain) - .disabled(true) + ActionButton(title: "Scanning...", icon: nil, color: .pmCard, isLoading: true, disabled: true) {} case .completed: if vm.totalSelectedSize > 0 { - GradientActionButton( - title: "Clean (\(ByteCountFormatter.string(fromByteCount: vm.totalSelectedSize, countStyle: .file)))", + ActionButton( + title: "Clean \(ByteCountFormatter.string(fromByteCount: vm.totalSelectedSize, countStyle: .file))", icon: "trash.fill", - gradient: AppGradients.accent + color: .pmAccent ) { - withAnimation(.pmSpring) { - vm.cleanAll() - } + withAnimation(.pmSpring) { vm.cleanAll() } } - Button(action: { - withAnimation(.pmSpring) { - vm.startSmartScan() - } - }) { - Text("Re-scan") - .font(.pmBody) - .foregroundColor(.pmTextSecondary) - .frame(height: 44) - .padding(.horizontal, 24) - .background(Color.pmCard) - .cornerRadius(12) + SecondaryButton(title: "Re-scan") { + withAnimation(.pmSpring) { vm.startSmartScan() } } - .buttonStyle(.plain) } else { - GradientActionButton( - title: "Scan Again", - icon: "arrow.clockwise", - gradient: AppGradients.success - ) { - withAnimation(.pmSpring) { - vm.startSmartScan() - } + ActionButton(title: "Scan Again", icon: "arrow.clockwise", color: .pmSuccess) { + withAnimation(.pmSpring) { vm.startSmartScan() } } } case .cleaning: - Button(action: {}) { - HStack(spacing: 8) { - ProgressView() - .progressViewStyle(.circular) - .scaleEffect(0.7) - .tint(.white) - Text("Cleaning...") - .font(.pmSubheadline) - .foregroundColor(.pmTextSecondary) - } - .frame(width: 200, height: 44) - .background(Color.pmCard) - .cornerRadius(12) - } - .buttonStyle(.plain) - .disabled(true) + ActionButton(title: "Cleaning...", icon: nil, color: .pmCard, isLoading: true, disabled: true) {} case .cleaned: - GradientActionButton( - title: "Done", - icon: "checkmark", - gradient: AppGradients.success - ) { - withAnimation(.pmSpring) { - vm.scanState = .idle - } + ActionButton(title: "Done", icon: "checkmark", color: .pmSuccess) { + withAnimation(.pmSpring) { vm.scanState = .idle } } } } @@ -443,7 +470,7 @@ struct DiskStatCard: View { let color: Color var body: some View { - VStack(spacing: 8) { + VStack(spacing: 10) { Image(systemName: icon) .font(.system(size: 16)) .foregroundColor(color) @@ -456,13 +483,19 @@ struct DiskStatCard: View { .font(.system(size: 10, weight: .medium)) .foregroundColor(.pmTextMuted) } - .frame(width: 100, height: 80) - .background(Color.pmCard.opacity(0.5)) - .cornerRadius(12) + .frame(width: 110, height: 90) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color.pmCard.opacity(0.5)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.pmSeparator.opacity(0.4), lineWidth: 0.5) + ) + ) } } -// MARK: - Result Row (Clickable) +// MARK: - Result Row struct ResultRow: View { @EnvironmentObject var vm: AppViewModel @@ -484,8 +517,8 @@ struct ResultRow: View { } var body: some View { - HStack(spacing: 12) { - // Category checkbox + HStack(spacing: 10) { + // Checkbox Button(action: { withAnimation(.pmSmooth) { if isFullySelected { @@ -497,24 +530,22 @@ struct ResultRow: View { }) { ZStack { RoundedRectangle(cornerRadius: 4) - .stroke(isCategorySelected ? result.category.color : Color.pmTextMuted, lineWidth: 1.5) - .frame(width: 18, height: 18) + .stroke(isCategorySelected ? result.category.color : Color.pmTextMuted.opacity(0.5), lineWidth: 1.5) + .frame(width: 16, height: 16) if isFullySelected { RoundedRectangle(cornerRadius: 4) .fill(result.category.color) - .frame(width: 18, height: 18) - + .frame(width: 16, height: 16) Image(systemName: "checkmark") - .font(.system(size: 10, weight: .bold)) + .font(.system(size: 9, weight: .bold)) .foregroundColor(.white) } else if isCategorySelected { RoundedRectangle(cornerRadius: 4) .fill(result.category.color) - .frame(width: 18, height: 18) - + .frame(width: 16, height: 16) Image(systemName: "minus") - .font(.system(size: 10, weight: .bold)) + .font(.system(size: 9, weight: .bold)) .foregroundColor(.white) } } @@ -522,12 +553,12 @@ struct ResultRow: View { } .buttonStyle(.plain) - // Category icon + info (clickable to navigate) + // Category row (clickable to navigate) Button(action: onTap) { - HStack(spacing: 12) { + HStack(spacing: 10) { ZStack { - RoundedRectangle(cornerRadius: 6) - .fill(result.category.color.opacity(isCategorySelected ? 0.15 : 0.05)) + RoundedRectangle(cornerRadius: 7) + .fill(result.category.color.opacity(isCategorySelected ? 0.12 : 0.06)) .frame(width: 28, height: 28) Image(systemName: result.category.icon) @@ -535,39 +566,39 @@ struct ResultRow: View { .foregroundColor(isCategorySelected ? result.category.color : .pmTextMuted) } - VStack(alignment: .leading, spacing: 2) { + VStack(alignment: .leading, spacing: 1) { Text(LocalizedStringKey(result.category.rawValue)) - .font(.pmBody) + .font(.system(size: 12, weight: .medium, design: .rounded)) .foregroundColor(isCategorySelected ? .pmTextPrimary : .pmTextMuted) Text("\(vm.selectedCountInCategory(result.category))/\(result.itemCount) items") - .font(.system(size: 10)) + .font(.system(size: 9, weight: .medium)) .foregroundColor(.pmTextMuted) } Spacer() Text(ByteCountFormatter.string(fromByteCount: selectedSize, countStyle: .file)) - .font(.system(size: 14, weight: .semibold, design: .rounded)) + .font(.system(size: 12, weight: .semibold, design: .rounded)) .foregroundColor(isCategorySelected ? result.category.color : .pmTextMuted) Image(systemName: "chevron.right") - .font(.system(size: 10, weight: .semibold)) - .foregroundColor(isHovering ? result.category.color : .pmTextMuted) + .font(.system(size: 9, weight: .semibold)) + .foregroundColor(isHovering ? result.category.color : .pmTextMuted.opacity(0.5)) } .contentShape(Rectangle()) } .buttonStyle(.plain) } - .padding(.horizontal, 16) - .padding(.vertical, 10) + .padding(.horizontal, 14) + .padding(.vertical, 9) .background( RoundedRectangle(cornerRadius: 10) .fill(isHovering ? Color.pmCardHover : Color.pmCard.opacity(0.4)) - ) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(isHovering ? result.category.color.opacity(0.3) : Color.clear, lineWidth: 1) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(isHovering ? result.category.color.opacity(0.2) : Color.pmSeparator.opacity(0.3), lineWidth: 0.5) + ) ) .onHover { h in withAnimation(.pmSmooth) { isHovering = h } @@ -575,7 +606,85 @@ struct ResultRow: View { } } -// MARK: - Gradient Action Button +// MARK: - Action Button + +struct ActionButton: View { + let title: LocalizedStringKey + let icon: String? + let color: Color + var isLoading: Bool = false + var disabled: Bool = false + let action: () -> Void + + @State private var isHovering = false + + var body: some View { + Button(action: action) { + HStack(spacing: 8) { + if isLoading { + ProgressView() + .progressViewStyle(.circular) + .scaleEffect(0.6) + .tint(.white) + } + if let icon = icon { + Image(systemName: icon) + .font(.system(size: 13, weight: .semibold)) + } + Text(title) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + } + .foregroundColor(.white) + .frame(height: 42) + .padding(.horizontal, 28) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(color) + ) + .scaleEffect(isHovering && !disabled ? 1.02 : 1.0) + .shadow(color: color.opacity(isHovering ? 0.25 : 0.12), radius: isHovering ? 10 : 5, y: isHovering ? 4 : 2) + } + .buttonStyle(.plain) + .disabled(disabled) + .opacity(disabled ? 0.7 : 1) + .onHover { h in + withAnimation(.pmSmooth) { isHovering = h } + } + } +} + +// MARK: - Secondary Button + +struct SecondaryButton: View { + let title: String + let action: () -> Void + + @State private var isHovering = false + + var body: some View { + Button(action: action) { + Text(title) + .font(.system(size: 13, weight: .medium, design: .rounded)) + .foregroundColor(.pmTextSecondary) + .frame(height: 42) + .padding(.horizontal, 20) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(Color.pmCard) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.pmSeparator.opacity(0.5), lineWidth: 0.5) + ) + ) + } + .buttonStyle(.plain) + .onHover { h in + withAnimation(.pmSmooth) { isHovering = h } + } + } +} + +// MARK: - Legacy Gradient Button (kept for CategoryDetailView compatibility) struct GradientActionButton: View { let title: LocalizedStringKey @@ -589,17 +698,19 @@ struct GradientActionButton: View { Button(action: action) { HStack(spacing: 8) { Image(systemName: icon) - .font(.system(size: 14, weight: .semibold)) + .font(.system(size: 13, weight: .semibold)) Text(title) - .font(.system(size: 15, weight: .semibold, design: .rounded)) + .font(.system(size: 14, weight: .semibold, design: .rounded)) } .foregroundColor(.white) - .frame(height: 44) - .padding(.horizontal, 32) - .background(gradient) - .cornerRadius(12) - .scaleEffect(isHovering ? 1.03 : 1.0) - .pmShadow(radius: isHovering ? 15 : 8, y: isHovering ? 6 : 4) + .frame(height: 42) + .padding(.horizontal, 28) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(gradient) + ) + .scaleEffect(isHovering ? 1.02 : 1.0) + .pmShadow(radius: isHovering ? 10 : 5, y: isHovering ? 4 : 2) } .buttonStyle(.plain) .onHover { h in diff --git a/PureMac/en.lproj/Localizable.strings b/PureMac/en.lproj/Localizable.strings index ad8b138..4bcdd36 100644 --- a/PureMac/en.lproj/Localizable.strings +++ b/PureMac/en.lproj/Localizable.strings @@ -10,16 +10,21 @@ /* Sidebar */ "CLEANING" = "CLEANING"; +"System Cleaner" = "System Cleaner"; "Last cleaned: %@" = "Last cleaned: %@"; +"Cleaned %@" = "Cleaned %@"; /* Smart Scan */ "Smart Scan" = "Smart Scan"; "Click Scan to start" = "Click Scan to start"; +"Analyze all categories" = "Analyze all categories"; +"Scan everything at once" = "Scan everything at once"; "Total" = "Total"; "Used" = "Used"; "Free" = "Free"; "Purgeable" = "Purgeable"; "junk found" = "junk found"; +"All clean" = "All clean"; "Your Mac is clean!" = "Your Mac is clean!"; "freed up" = "freed up"; "Cleaning..." = "Cleaning..."; @@ -43,6 +48,41 @@ "Done" = "Done"; "Clean (%@)" = "Clean (%@)"; "Clean %lld items (%@)" = "Clean %lld items (%@)"; +"Continue" = "Continue"; +"Skip" = "Skip"; +"Start Scanning" = "Start Scanning"; +"Uninstall" = "Uninstall"; + +/* Onboarding */ +"Welcome to PureMac" = "Welcome to PureMac"; +"Keep your Mac fast, clean, and optimized." = "Keep your Mac fast, clean, and optimized."; +"One-Click Clean" = "One-Click Clean"; +"Find junk in seconds" = "Find junk in seconds"; +"Remove files safely" = "Remove files safely"; +"Delete apps completely" = "Delete apps completely"; +"Full Disk Access" = "Full Disk Access"; +"Access Granted" = "Access Granted"; +"PureMac can now scan all areas of your Mac." = "PureMac can now scan all areas of your Mac."; +"PureMac needs Full Disk Access to scan Trash, Mail, Desktop, Documents, and Homebrew cache." = "PureMac needs Full Disk Access to scan Trash, Mail, Desktop, Documents, and Homebrew cache."; +"Open System Settings" = "Open System Settings"; +"Click \"Open System Settings\" below" = "Click \"Open System Settings\" below"; +"Find PureMac in the list" = "Find PureMac in the list"; +"Toggle the switch to enable access" = "Toggle the switch to enable access"; +"You're All Set" = "You're All Set"; +"PureMac is ready to keep your Mac clean." = "PureMac is ready to keep your Mac clean."; +"Limited Access" = "Limited Access"; + +/* App Uninstaller */ +"App Uninstaller" = "App Uninstaller"; +"Uninstall apps and their leftover files" = "Uninstall apps and their leftover files"; +"Remove apps and all their associated data" = "Remove apps and all their associated data"; +"Search apps..." = "Search apps..."; +"Scanning applications..." = "Scanning applications..."; +"No removable apps found" = "No removable apps found"; +"No apps matching \"%@\"" = "No apps matching \"%@\""; +"Uninstall %@?" = "Uninstall %@?"; +"%lld related" = "%lld related"; +"%@ apps" = "%@ apps"; /* Settings - Schedule */ "Schedule" = "Schedule"; @@ -67,6 +107,10 @@ /* Settings - General */ "General" = "General"; +"Appearance" = "Appearance"; +"System" = "System"; +"Light" = "Light"; +"Dark" = "Dark"; "App Behavior" = "App Behavior"; "Launch at login" = "Launch at login"; "Show in Dock" = "Show in Dock"; @@ -76,7 +120,6 @@ /* Settings - About */ "About" = "About"; -"Version 1.0.0" = "Version 1.0.0"; "A free, open-source Mac cleaning utility.\nKeep your Mac fast, clean, and optimized." = "A free, open-source Mac cleaning utility.\nKeep your Mac fast, clean, and optimized."; "GitHub Repository" = "GitHub Repository"; "MIT License" = "MIT License"; @@ -93,7 +136,6 @@ "Brew Cache" = "Brew Cache"; /* Category Descriptions */ -"Scan everything at once" = "Scan everything at once"; "System caches, logs, and temporary files" = "System caches, logs, and temporary files"; "Application caches and browser data" = "Application caches and browser data"; "Logs, caches, and temporary files from local AI apps" = "Logs, caches, and temporary files from local AI apps"; diff --git a/PureMac/zh-Hans.lproj/Localizable.strings b/PureMac/zh-Hans.lproj/Localizable.strings index 353e5f2..2cec0e4 100644 --- a/PureMac/zh-Hans.lproj/Localizable.strings +++ b/PureMac/zh-Hans.lproj/Localizable.strings @@ -114,5 +114,53 @@ "Every 2 Weeks" = "每两周"; "Monthly" = "每月"; +/* Sidebar */ +"System Cleaner" = "系统清理工具"; +"Cleaned %@" = "已清理 %@"; + +/* Smart Scan - New */ +"Analyze all categories" = "分析所有类别"; +"All clean" = "全部干净"; + +/* Onboarding */ +"Welcome to PureMac" = "欢迎使用 PureMac"; +"Keep your Mac fast, clean, and optimized." = "让您的 Mac 保持快速、干净和优化。"; +"One-Click Clean" = "一键清理"; +"Find junk in seconds" = "秒速发现垃圾"; +"Remove files safely" = "安全删除文件"; +"Delete apps completely" = "彻底删除应用"; +"Full Disk Access" = "完整磁盘访问权限"; +"Access Granted" = "已授予访问权限"; +"PureMac can now scan all areas of your Mac." = "PureMac 现在可以扫描您 Mac 的所有区域。"; +"Open System Settings" = "打开系统设置"; +"Click \"Open System Settings\" below" = "点击下方「打开系统设置」"; +"Find PureMac in the list" = "在列表中找到 PureMac"; +"Toggle the switch to enable access" = "打开开关以启用访问权限"; +"You're All Set" = "全部就绪"; +"PureMac is ready to keep your Mac clean." = "PureMac 已准备好保持您的 Mac 干净。"; +"Limited Access" = "有限访问"; +"Continue" = "继续"; +"Skip" = "跳过"; +"Start Scanning" = "开始扫描"; + +/* App Uninstaller */ +"App Uninstaller" = "应用卸载"; +"Uninstall apps and their leftover files" = "卸载应用及其残留文件"; +"Remove apps and all their associated data" = "删除应用及其所有关联数据"; +"Search apps..." = "搜索应用..."; +"Scanning applications..." = "正在扫描应用..."; +"No removable apps found" = "未找到可卸载的应用"; +"Uninstall" = "卸载"; + +/* Settings - General - New */ +"Appearance" = "外观"; +"System" = "系统"; +"Light" = "浅色"; +"Dark" = "深色"; + +/* Cleaning Categories - New */ +"AI Apps" = "AI 应用"; +"Logs, caches, and temporary files from local AI apps" = "本地 AI 应用的日志、缓存和临时文件"; + /* Notifications */ "Found %@ of junk files." = "发现了 %@ 的垃圾文件。"; diff --git a/PureMac/zh-Hant.lproj/Localizable.strings b/PureMac/zh-Hant.lproj/Localizable.strings index ced5944..539acbb 100644 --- a/PureMac/zh-Hant.lproj/Localizable.strings +++ b/PureMac/zh-Hant.lproj/Localizable.strings @@ -112,5 +112,53 @@ "Every 2 Weeks" = "每兩週"; "Monthly" = "每月"; +/* Sidebar */ +"System Cleaner" = "系統清理工具"; +"Cleaned %@" = "已清理 %@"; + +/* Smart Scan - New */ +"Analyze all categories" = "分析所有類別"; +"All clean" = "全部乾淨"; + +/* Onboarding */ +"Welcome to PureMac" = "歡迎使用 PureMac"; +"Keep your Mac fast, clean, and optimized." = "讓您的 Mac 保持快速、乾淨且最佳化。"; +"One-Click Clean" = "一鍵清理"; +"Find junk in seconds" = "秒速發現垃圾"; +"Remove files safely" = "安全刪除檔案"; +"Delete apps completely" = "徹底刪除應用程式"; +"Full Disk Access" = "完整磁碟存取權限"; +"Access Granted" = "已授予存取權限"; +"PureMac can now scan all areas of your Mac." = "PureMac 現在可以掃描您 Mac 的所有區域。"; +"Open System Settings" = "開啟系統設定"; +"Click \"Open System Settings\" below" = "點擊下方「開啟系統設定」"; +"Find PureMac in the list" = "在列表中找到 PureMac"; +"Toggle the switch to enable access" = "開啟開關以啟用存取權限"; +"You're All Set" = "全部就緒"; +"PureMac is ready to keep your Mac clean." = "PureMac 已準備好保持您的 Mac 乾淨。"; +"Limited Access" = "有限存取"; +"Continue" = "繼續"; +"Skip" = "略過"; +"Start Scanning" = "開始掃描"; + +/* App Uninstaller */ +"App Uninstaller" = "應用程式解除安裝"; +"Uninstall apps and their leftover files" = "解除安裝應用程式及其殘留檔案"; +"Remove apps and all their associated data" = "移除應用程式及其所有相關資料"; +"Search apps..." = "搜尋應用程式…"; +"Scanning applications..." = "正在掃描應用程式…"; +"No removable apps found" = "未找到可移除的應用程式"; +"Uninstall" = "解除安裝"; + +/* Settings - General - New */ +"Appearance" = "外觀"; +"System" = "系統"; +"Light" = "淺色"; +"Dark" = "深色"; + +/* Cleaning Categories - New */ +"AI Apps" = "AI 應用程式"; +"Logs, caches, and temporary files from local AI apps" = "本地 AI 應用程式的記錄檔、快取及暫存檔案"; + /* Notifications */ "Found %@ of junk files." = "發現 %@ 的垃圾檔案。"; diff --git a/project.yml b/project.yml index 6f6fa42..13da266 100644 --- a/project.yml +++ b/project.yml @@ -19,8 +19,8 @@ settings: CODE_SIGN_ENTITLEMENTS: "PureMac/PureMac.entitlements" INFOPLIST_FILE: "PureMac/Info.plist" PRODUCT_BUNDLE_IDENTIFIER: "com.puremac.app" - MARKETING_VERSION: "1.0.1" - CURRENT_PROJECT_VERSION: "3" + MARKETING_VERSION: "1.1.0" + CURRENT_PROJECT_VERSION: "4" GENERATE_INFOPLIST_FILE: "NO" ASSETCATALOG_COMPILER_APPICON_NAME: "AppIcon" COMBINE_HIDPI_IMAGES: "YES"