diff --git a/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..92d8298 --- /dev/null +++ b/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "Icon-16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "Icon-32 1.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "Icon-32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "Icon-64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "Icon-128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "Icon-256 1.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "Icon-256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "Icon-512 1.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "Icon-512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "Icon-1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png new file mode 100644 index 0000000..f1bdca3 Binary files /dev/null and b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png differ diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-128.png b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-128.png new file mode 100644 index 0000000..749ff7c Binary files /dev/null and b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-128.png differ diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-16.png b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-16.png new file mode 100644 index 0000000..efbe3c0 Binary files /dev/null and b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-16.png differ diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-256 1.png b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-256 1.png new file mode 100644 index 0000000..6eb8abe Binary files /dev/null and b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-256 1.png differ diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-256.png b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-256.png new file mode 100644 index 0000000..6eb8abe Binary files /dev/null and b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-256.png differ diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-32 1.png b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-32 1.png new file mode 100644 index 0000000..5071f0b Binary files /dev/null and b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-32 1.png differ diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-32.png b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-32.png new file mode 100644 index 0000000..5071f0b Binary files /dev/null and b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-32.png differ diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-512 1.png b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-512 1.png new file mode 100644 index 0000000..69cdc73 Binary files /dev/null and b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-512 1.png differ diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-512.png b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-512.png new file mode 100644 index 0000000..69cdc73 Binary files /dev/null and b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-512.png differ diff --git a/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-64.png b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-64.png new file mode 100644 index 0000000..f51283e Binary files /dev/null and b/App/Resources/Assets.xcassets/AppIcon.appiconset/Icon-64.png differ diff --git a/App/Resources/Assets.xcassets/Contents.json b/App/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/App/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/App/Resources/FengNiao.entitlements b/App/Resources/FengNiao.entitlements new file mode 100644 index 0000000..19afff1 --- /dev/null +++ b/App/Resources/FengNiao.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + + diff --git a/App/fengniao.xcodeproj/project.pbxproj b/App/fengniao.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e88b0cc --- /dev/null +++ b/App/fengniao.xcodeproj/project.pbxproj @@ -0,0 +1,391 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 6AAF22A82AA1D0F400DB0B5F /* FengNiaoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AAF22A72AA1D0F400DB0B5F /* FengNiaoApp.swift */; }; + 6AAF22AA2AA1D0F400DB0B5F /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AAF22A92AA1D0F400DB0B5F /* AppView.swift */; }; + 6AAF22AC2AA1D0F500DB0B5F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6AAF22AB2AA1D0F500DB0B5F /* Assets.xcassets */; }; + 6AAF22AF2AA1D0F500DB0B5F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6AAF22AE2AA1D0F500DB0B5F /* Preview Assets.xcassets */; }; + 6AE869FC2AA1D41600DCF78B /* FengNiaoKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6AE869FB2AA1D41600DCF78B /* FengNiaoKit */; }; + 6AE869FE2AA1D58E00DCF78B /* AppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE869FD2AA1D58E00DCF78B /* AppViewModel.swift */; }; + 6AE86A002AA1D5D800DCF78B /* DeleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE869FF2AA1D5D800DCF78B /* DeleteView.swift */; }; + 6AE86A022AA1D5F700DCF78B /* DeleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE86A012AA1D5F700DCF78B /* DeleteViewModel.swift */; }; + 6AE86A042AA1D61600DCF78B /* ExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE86A032AA1D61600DCF78B /* ExportView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 6AAF22A42AA1D0F400DB0B5F /* FengNiao.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FengNiao.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6AAF22A72AA1D0F400DB0B5F /* FengNiaoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FengNiaoApp.swift; sourceTree = ""; }; + 6AAF22A92AA1D0F400DB0B5F /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = ""; }; + 6AAF22AB2AA1D0F500DB0B5F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 6AAF22AE2AA1D0F500DB0B5F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 6AAF22B02AA1D0F500DB0B5F /* FengNiao.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FengNiao.entitlements; sourceTree = ""; }; + 6AE869F92AA1D35500DCF78B /* FengNiao */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FengNiao; path = ..; sourceTree = ""; }; + 6AE869FD2AA1D58E00DCF78B /* AppViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewModel.swift; sourceTree = ""; }; + 6AE869FF2AA1D5D800DCF78B /* DeleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteView.swift; sourceTree = ""; }; + 6AE86A012AA1D5F700DCF78B /* DeleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteViewModel.swift; sourceTree = ""; }; + 6AE86A032AA1D61600DCF78B /* ExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6AAF22A12AA1D0F400DB0B5F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6AE869FC2AA1D41600DCF78B /* FengNiaoKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6AAF229B2AA1D0F400DB0B5F = { + isa = PBXGroup; + children = ( + 6AE869F92AA1D35500DCF78B /* FengNiao */, + 6AAF22A62AA1D0F400DB0B5F /* macOS */, + 6AAF22A52AA1D0F400DB0B5F /* Products */, + 6AAF22B62AA1D1C900DB0B5F /* Resources */, + 6AE869FA2AA1D41600DCF78B /* Frameworks */, + ); + sourceTree = ""; + }; + 6AAF22A52AA1D0F400DB0B5F /* Products */ = { + isa = PBXGroup; + children = ( + 6AAF22A42AA1D0F400DB0B5F /* FengNiao.app */, + ); + name = Products; + sourceTree = ""; + }; + 6AAF22A62AA1D0F400DB0B5F /* macOS */ = { + isa = PBXGroup; + children = ( + 6AAF22A92AA1D0F400DB0B5F /* AppView.swift */, + 6AE869FD2AA1D58E00DCF78B /* AppViewModel.swift */, + 6AE869FF2AA1D5D800DCF78B /* DeleteView.swift */, + 6AE86A012AA1D5F700DCF78B /* DeleteViewModel.swift */, + 6AE86A032AA1D61600DCF78B /* ExportView.swift */, + 6AAF22A72AA1D0F400DB0B5F /* FengNiaoApp.swift */, + 6AAF22AD2AA1D0F500DB0B5F /* Preview Content */, + ); + path = macOS; + sourceTree = ""; + }; + 6AAF22AD2AA1D0F500DB0B5F /* Preview Content */ = { + isa = PBXGroup; + children = ( + 6AAF22AE2AA1D0F500DB0B5F /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 6AAF22B62AA1D1C900DB0B5F /* Resources */ = { + isa = PBXGroup; + children = ( + 6AAF22B02AA1D0F500DB0B5F /* FengNiao.entitlements */, + 6AAF22AB2AA1D0F500DB0B5F /* Assets.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; + 6AE869FA2AA1D41600DCF78B /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6AAF22A32AA1D0F400DB0B5F /* fengniao */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6AAF22B32AA1D0F500DB0B5F /* Build configuration list for PBXNativeTarget "fengniao" */; + buildPhases = ( + 6AAF22A02AA1D0F400DB0B5F /* Sources */, + 6AAF22A12AA1D0F400DB0B5F /* Frameworks */, + 6AAF22A22AA1D0F400DB0B5F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = fengniao; + packageProductDependencies = ( + 6AE869FB2AA1D41600DCF78B /* FengNiaoKit */, + ); + productName = fengniao; + productReference = 6AAF22A42AA1D0F400DB0B5F /* FengNiao.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6AAF229C2AA1D0F400DB0B5F /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + TargetAttributes = { + 6AAF22A32AA1D0F400DB0B5F = { + CreatedOnToolsVersion = 14.3; + }; + }; + }; + buildConfigurationList = 6AAF229F2AA1D0F400DB0B5F /* Build configuration list for PBXProject "fengniao" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 6AAF229B2AA1D0F400DB0B5F; + productRefGroup = 6AAF22A52AA1D0F400DB0B5F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6AAF22A32AA1D0F400DB0B5F /* fengniao */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6AAF22A22AA1D0F400DB0B5F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6AAF22AF2AA1D0F500DB0B5F /* Preview Assets.xcassets in Resources */, + 6AAF22AC2AA1D0F500DB0B5F /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6AAF22A02AA1D0F400DB0B5F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6AE86A022AA1D5F700DCF78B /* DeleteViewModel.swift in Sources */, + 6AAF22AA2AA1D0F400DB0B5F /* AppView.swift in Sources */, + 6AE86A042AA1D61600DCF78B /* ExportView.swift in Sources */, + 6AE86A002AA1D5D800DCF78B /* DeleteView.swift in Sources */, + 6AAF22A82AA1D0F400DB0B5F /* FengNiaoApp.swift in Sources */, + 6AE869FE2AA1D58E00DCF78B /* AppViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 6AAF22B12AA1D0F500DB0B5F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 6AAF22B22AA1D0F500DB0B5F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 6AAF22B42AA1D0F500DB0B5F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Resources/FengNiao.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"macOS/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.onevcat.fengniao; + PRODUCT_NAME = FengNiao; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 6AAF22B52AA1D0F500DB0B5F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Resources/FengNiao.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"macOS/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.onevcat.fengniao; + PRODUCT_NAME = FengNiao; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6AAF229F2AA1D0F400DB0B5F /* Build configuration list for PBXProject "fengniao" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6AAF22B12AA1D0F500DB0B5F /* Debug */, + 6AAF22B22AA1D0F500DB0B5F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6AAF22B32AA1D0F500DB0B5F /* Build configuration list for PBXNativeTarget "fengniao" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6AAF22B42AA1D0F500DB0B5F /* Debug */, + 6AAF22B52AA1D0F500DB0B5F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6AE869FB2AA1D41600DCF78B /* FengNiaoKit */ = { + isa = XCSwiftPackageProductDependency; + productName = FengNiaoKit; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 6AAF229C2AA1D0F400DB0B5F /* Project object */; +} diff --git a/App/fengniao.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/App/fengniao.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/App/fengniao.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/App/fengniao.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/App/fengniao.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/App/fengniao.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/App/fengniao.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/fengniao.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..c6062d0 --- /dev/null +++ b/App/fengniao.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,43 @@ +{ + "object": { + "pins": [ + { + "package": "CommandLineKit", + "repositoryURL": "https://github.com/benoit-pereira-da-silva/CommandLine.git", + "state": { + "branch": null, + "revision": "3eaafd5941e359f025a411e3b5947f96d82d1bc9", + "version": "4.0.9" + } + }, + { + "package": "PathKit", + "repositoryURL": "https://github.com/kylef/PathKit.git", + "state": { + "branch": null, + "revision": "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", + "version": "1.0.1" + } + }, + { + "package": "Rainbow", + "repositoryURL": "https://github.com/onevcat/Rainbow.git", + "state": { + "branch": null, + "revision": "626c3d4b6b55354b4af3aa309f998fae9b31a3d9", + "version": "3.2.0" + } + }, + { + "package": "Spectre", + "repositoryURL": "https://github.com/kylef/Spectre.git", + "state": { + "branch": null, + "revision": "26cc5e9ae0947092c7139ef7ba612e34646086c7", + "version": "0.10.1" + } + } + ] + }, + "version": 1 +} diff --git a/App/fengniao.xcodeproj/xcshareddata/xcschemes/FengNiao-macOS.xcscheme b/App/fengniao.xcodeproj/xcshareddata/xcschemes/FengNiao-macOS.xcscheme new file mode 100644 index 0000000..ae34a81 --- /dev/null +++ b/App/fengniao.xcodeproj/xcshareddata/xcschemes/FengNiao-macOS.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/App/macOS/AppView.swift b/App/macOS/AppView.swift new file mode 100644 index 0000000..c3d5b29 --- /dev/null +++ b/App/macOS/AppView.swift @@ -0,0 +1,393 @@ +import SwiftUI +import FengNiaoKit +import PathKit +import QuickLook + +struct AppView: View { + @StateObject private var viewModel: AppViewModel = AppViewModel() + + // MARK: Text Field + + @State private var projectPath: String = "" + @State private var excludePaths: String = "" + @State private var resourcesExtensions: String = Constants.defaultResourcesExtension + @FocusState private var focusedField: FocusedField? + + // MARK: Show alert + + @State private var showDeleteAllAlert: Bool = false + @State private var showDeleteAlert: Bool = false + @State private var showErrorAlert: Bool = false + + // MARK: Toggle View + + @State private var showDeleteAllView: Bool = false + @State private var showDeleteView: Bool = false + @State private var showExportView: Bool = false + + // MARK: Table + + @State private var fileSelection = Set() + @State private var fileNameSortOrder = [ + KeyPathComparator(\FengNiaoKit.FileInfo.fileName), + KeyPathComparator(\FengNiaoKit.FileInfo.size), + KeyPathComparator(\FengNiaoKit.FileInfo.path) + ] + @State private var previewImageUrl: URL? + + // MARK: Checkbox + + @State private var toggleStates = [ + ToggleState(fileExtension: "h", isOn: true), + ToggleState(fileExtension: "m", isOn: true), + ToggleState(fileExtension: "mm", isOn: true), + ToggleState(fileExtension: "swift", isOn: true), + ToggleState(fileExtension: "xib", isOn: true), + ToggleState(fileExtension: "storyboard", isOn: true), + ToggleState(fileExtension: "plist", isOn: true), + ToggleState(fileExtension: "pbxproj", isOn: true) + ] + + // MARK: - View + + var body: some View { + VStack { + configView + + Divider() + + unusedResourcesTable + + Spacer(minLength: 16) + + resultView + } + .animation(.default, value: viewModel.contentState) + .padding() + .onChange(of: viewModel.contentState) { state in + if state == .error { + showErrorAlert.toggle() + } + } + .sheet(isPresented: $showDeleteAllView) { + deleteView(filesToDelete: viewModel.unusedFiles) + .onDisappear { + fetchUnusedFiles() + } + } + .sheet(isPresented: $showDeleteView) { + let fileToDelete = viewModel.unusedFiles.filter { fileSelection.contains($0.id) } + deleteView(filesToDelete: fileToDelete) + .onDisappear { + viewModel.unusedFiles.removeAll(where: { fileToDelete.contains($0) } ) + fileSelection = [] + } + } + .sheet(isPresented: $showExportView) { + ExportView(unusedFiles: viewModel.unusedFiles) + .frame(minWidth: 500, minHeight: 300, idealHeight: 500) + } + .alert( + deleteItemTitle, + isPresented: $showDeleteAlert + ) { + Button("Delete", role: .destructive) { + showDeleteView.toggle() + } + Button("Cancel", role: .cancel) {} + } message: { + Text("This item will be delete immediately.\nYou can't undo this action.") + } + .alert( + "Are you sure you want to delete all items?", + isPresented: $showDeleteAllAlert + ) { + Button("Delete All") { + showDeleteAllView.toggle() + } + Button("Cancel", role: .cancel) {} + } message: { + Text("You can't undo this action.") + } + .alert( + "Something went wrong", + isPresented: $showErrorAlert + ) { + // Only OK action, no need to implement + } message: { + if let error = viewModel.error as? FengNiaoError { + switch error { + case .noResourceExtension: + Text("You need to specify some resource extensions as search target.") + case .noFileExtension: + Text("You need to specify some file extensions to search in.") + } + } else { + Text("Unknown Error: \(viewModel.error?.localizedDescription ?? "")") + } + } + } + + @ViewBuilder + private var configView: some View { + VStack(alignment: .leading) { + Text("Configurations") + .font(.headline) + + HStack { + Text("Project Path") + TextField("Root path of your Xcode project", text: $projectPath) + .focused($focusedField, equals: .project) + Button("Browse...") { + handleOpenFile() + } + .disabled(viewModel.isLoading) + } + + HStack { + Text("Exclude Paths") + TextField( + "Exclude paths from search, separates with space. Example: Pods Carthage", + text: $excludePaths + ) + .focused($focusedField, equals: .excludes) + } + + HStack { + Text("File Extensions") + ForEach(toggleStates.indices, id: \.self) { index in + Toggle(toggleStates[index].fileExtension, isOn: $toggleStates[index].isOn) + } + } + + HStack { + Text("Resources Extensions") + TextField( + "Resource file extensions, separates with space. Default is 'imageset jpg png gif pdf'", + text: $resourcesExtensions + ) + .focused($focusedField, equals: .resources) + Button("Restore") { + resourcesExtensions = Constants.defaultResourcesExtension + } + } + HStack { + Spacer() + Button(viewModel.isLoading ? "Searching... " : "Search...") { + if projectPath.isEmpty { + focusedField = .project + return + } + fetchUnusedFiles() + focusedField = nil + } + .disabled(viewModel.isLoading) + .tint(Color.accentColor) + .buttonStyle(.borderedProminent) + } + } + .onTapGesture { + focusedField = nil + } + } + + @ViewBuilder + private var unusedResourcesTable: some View { + VStack(alignment: .leading) { + Text("Unused Files") + .font(.headline) + Table( + viewModel.unusedFiles, + selection: $fileSelection, + sortOrder: $fileNameSortOrder + ) { + TableColumn("File Name", value: \.fileName) { file in + Text(file.fileName) + .contentShape(Rectangle()) + .help(file.fileName) + .contextMenu { + Button("Copy") { + let pasteboard = NSPasteboard.general + pasteboard.declareTypes([.string], owner: nil) + pasteboard.setString(file.fileName, forType: .string) + } + Button("Show in Finder") { + NSWorkspace.shared.open(URL(fileURLWithPath: file.path.string)) + } + } + } + .width(min: 150, ideal: 150, max: 300) + + TableColumn("Size", value: \.size) { + Text($0.size.fn_readableSize) + } + .width(min: 50, max: 150) + + TableColumn("Full Path", value: \.path.string) { file in + HStack { + Text(file.path.string) + Button { + previewImageUrl = URL(fileURLWithPath: file.path.string) + } label: { + Image(systemName: "eye") + } + .keyboardShortcut(.space) + } + .quickLookPreview($previewImageUrl) + .contentShape(Rectangle()) + .contextMenu { + Button("Copy") { + let pasteboard = NSPasteboard.general + pasteboard.declareTypes([.string], owner: nil) + pasteboard.setString(file.path.string, forType: .string) + } + Button("Show in Finder") { + NSWorkspace.shared.open(URL(fileURLWithPath: file.path.string)) + } + } + .help(file.path.string) + } + } + .animation(.default, value: viewModel.unusedFiles) + .onChange(of: fileNameSortOrder) { sortOrder in + viewModel.unusedFiles.sort(using: sortOrder) + } + } + } + + @ViewBuilder + private var resultView: some View { + if viewModel.contentState == .content { + HStack { + if viewModel.unusedFiles.isEmpty { + Text("🎉 You have no unused resources in path: \(Path(projectPath).absolute().string)") + } else { + let size = viewModel.unusedFiles + .reduce(0) { $0 + $1.size }.fn_readableSize + Text("\(viewModel.unusedFiles.count) files are found. Total Size: \(size)") + } + + Spacer() + + Button("Delete") { + showDeleteAlert.toggle() + } + .disabled(fileSelection.isEmpty) + .disabled(viewModel.unusedFiles.isEmpty) + + Button("Delete All") { + showDeleteAllAlert.toggle() + } + .disabled(viewModel.unusedFiles.isEmpty) + + Button("Export CSV") { + showExportView.toggle() + } + .disabled(viewModel.unusedFiles.isEmpty) + } + } else if viewModel.contentState == .loading { + HStack { + HStack(alignment: .center, spacing: 8) { + ProgressView() + .controlSize(.small) + Text("Searching unused file. This may take a while...") + } + Spacer() + } + } + } + + @ViewBuilder + private func deleteView(filesToDelete: [FengNiaoKit.FileInfo]) -> some View { + DeleteView( + projectPath: self.projectPath, + filesToDelete: filesToDelete + ) + .frame(width: 500, height: 200) + } + + // MARK: Side Effects - Private + + private var deleteItemTitle: String { + if fileSelection.count == 1 { + if let firstItem = fileSelection.first, + let selectedUnusedFile = viewModel.unusedFiles.first(where: { $0.id == firstItem } ) { + return "Are you sure you want to delete \"\(selectedUnusedFile.fileName)\"" + } + } else { + return "Are you sure you want to delete the \(fileSelection.count) selected items?" + } + return "" + } + + private func handleOpenFile() { + let panel = NSOpenPanel() + panel.allowsMultipleSelection = false + panel.canChooseDirectories = true + if panel.runModal() == .OK { + if let chosenFile = panel.url { + let path = chosenFile.path + projectPath = path + } + } + } + + private func fetchUnusedFiles() { + let fileExtensions: [String] = toggleStates + .filter { $0.isOn } + .map { $0.fileExtension } + + viewModel.fetchUnusedFiles( + from: projectPath, + excludePaths: excludePaths, + fileExtensions: fileExtensions, + resourcesExtensions: resourcesExtensions + ) + } +} + +// MARK: - FocusedField + +extension AppView { + enum FocusedField { + case project, excludes, resources + } +} + +// MARK: - Constants + +enum Constants { + static let defaultResourcesExtension: String = "imageset jpg png gif pdf heic" +} + +// MARK: - Preview + +struct MainContentView_Previews: PreviewProvider { + static var previews: some View { + AppView() + .frame(width: 800, height: 800) + } +} + +// MARK: - FengNiaoKit.FileInfo + Identifiable + Hashable + +extension FengNiaoKit.FileInfo: Identifiable, Hashable { + public var id: String { + path.string + } + + public static func == (lhs: FileInfo, rhs: FileInfo) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +// MARK: - ToggleState + +struct ToggleState: Hashable { + let fileExtension: String + var isOn: Bool +} diff --git a/App/macOS/AppViewModel.swift b/App/macOS/AppViewModel.swift new file mode 100644 index 0000000..c392216 --- /dev/null +++ b/App/macOS/AppViewModel.swift @@ -0,0 +1,58 @@ +import FengNiaoKit +import SwiftUI + +final class AppViewModel: ObservableObject { + @Published var unusedFiles: [FengNiaoKit.FileInfo] = [] + @Published private(set) var contentState: ContentState = .idling + @Published private(set) var error: Error? + /// Scanning project path to display info in result text. + private(set) var scanningProjectPath: String = "" + + var isLoading: Bool { + contentState == .loading + } + + private let queue = DispatchQueue(label: "com.onevcat.fengniao", attributes: .concurrent) + + func fetchUnusedFiles( + from path: String, + excludePaths: String, + fileExtensions: [String], + resourcesExtensions: String + ) { + contentState = .loading + + scanningProjectPath = path + queue.async { + let fengNiao = FengNiao( + projectPath: path, + excludedPaths: excludePaths.split(separator: " ").map(String.init), + resourceExtensions: resourcesExtensions.split(separator: " ").map(String.init), + searchInFileExtensions: fileExtensions + ) + + do { + let files = try fengNiao.unusedFiles() + + DispatchQueue.main.async { + self.unusedFiles = files + self.contentState = .content + } + } catch { + DispatchQueue.main.async { + self.contentState = .error + self.error = error + } + } + } + } +} + +extension AppViewModel { + enum ContentState: Equatable { + case idling + case loading + case content + case error + } +} diff --git a/App/macOS/DeleteView.swift b/App/macOS/DeleteView.swift new file mode 100644 index 0000000..721429f --- /dev/null +++ b/App/macOS/DeleteView.swift @@ -0,0 +1,82 @@ +import SwiftUI +import FengNiaoKit + +struct DeleteView: View { + @Environment(\.dismiss) private var dismiss + @State private var showDetailStatus: Bool = true + @StateObject private var viewModel: DeleteStatusViewModel + + init( + projectPath: String, + filesToDelete: [FengNiaoKit.FileInfo] + ) { + _viewModel = StateObject( + wrappedValue: DeleteStatusViewModel( + projectPath: projectPath, + unusedFilesToDelete: filesToDelete + ) + ) + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + Image(systemName: showDetailStatus ? "chevron.down" : "chevron.right") + .font(.subheadline) + Text("Deleting unused files...") + .font(.body) + .bold() + } + .contentShape(Rectangle()) + .onTapGesture { + showDetailStatus.toggle() + } + .animation(.spring(response: 0.15), value: showDetailStatus) + + if showDetailStatus { + Text(viewModel.consoleStatus) + .font(.subheadline) + .foregroundColor(Color(NSColor.secondaryLabelColor)) + .animation(.spring(response: 0.3), value: showDetailStatus) + } + + Spacer() + + ProgressView( + viewModel.deleteAmount == 100 ? "Finished!" : "Deleting...", + value: viewModel.deleteAmount, + total: 100 + ) + .foregroundColor(Color(NSColor.secondaryLabelColor)) + + Spacer() + + HStack { + Spacer() + Button("Done") { + dismiss() + } + .disabled(viewModel.deleteAmount < 100) + .tint(Color.accentColor) + .buttonStyle(.borderedProminent) + } + } + .padding() + .onAppear { + viewModel.deleteUnusedFiles() + } + .alert("Something went wrong", isPresented: $viewModel.showError) { + // only need OK button + } message: { + Text(viewModel.errorAlertMessage) + } + + } +} + +struct DeleteStatusView_Previews: PreviewProvider { + static var previews: some View { + DeleteView(projectPath: "", filesToDelete: []) + .frame(width: 500, height: 200) + } +} diff --git a/App/macOS/DeleteViewModel.swift b/App/macOS/DeleteViewModel.swift new file mode 100644 index 0000000..ed7eba6 --- /dev/null +++ b/App/macOS/DeleteViewModel.swift @@ -0,0 +1,80 @@ +import FengNiaoKit +import Foundation +import Combine +import PathKit + +final class DeleteStatusViewModel: ObservableObject { + @Published var unusedFilesToDelete: [FengNiaoKit.FileInfo] = [] + @Published var deleteAmount = 0.0 + @Published var consoleStatus: String = "" + @Published var showError: Bool = false + @Published var errorAlertMessage: String = "" + + private let projectPath: String + private let queue = DispatchQueue(label: "com.onevcat.fengniao", attributes: .concurrent) + private let timer = Timer.publish(every: 0.1, on: .current, in: .default).autoconnect() + private var cancellable: AnyCancellable? + + init( + projectPath: String, + unusedFilesToDelete: [FengNiaoKit.FileInfo] + ) { + self.projectPath = projectPath + self.unusedFilesToDelete = unusedFilesToDelete + } + + func deleteUnusedFiles() { + let increasePercentagePerFiles = unusedFilesToDelete.count / 100 + + queue.async { + let (deleted, failed) = FengNiao.delete(self.unusedFilesToDelete) + + // FengNiaoKit doesn't public functions to calculate the exact download percentage, + // so using this hack to make progress continually running + self.cancellable = self.timer.sink { [weak self] _ in + DispatchQueue.main.async { + guard let self else { return } + if self.deleteAmount < 99 { + self.deleteAmount += Double(increasePercentagePerFiles) + } + } + } + + guard failed.isEmpty else { + self.showError.toggle() + + self.errorAlertMessage = "\(self.unusedFilesToDelete.count - failed.count) unused files are deleted. But we encountered some error while deleting these \(failed.count) files:" + for (fileInfo, err) in failed { + self.errorAlertMessage += "\(fileInfo.path.string) - \(err.localizedDescription)" + self.errorAlertMessage += "\n" + } + return + } + + DispatchQueue.main.async { + let content = self.unusedFilesToDelete.count > 1 ? + "\(self.unusedFilesToDelete.count) unused files are deleted" : + "\(self.unusedFilesToDelete.first?.fileName ?? "") is deleted" + self.consoleStatus += content + } + + if let children = try? Path(self.projectPath).absolute().children() { + DispatchQueue.main.async { + self.consoleStatus += "\nNow deleting unused reference in project.pbxproj..." + } + + let xcodeprojFiles = children.filter { $0.lastComponent.hasSuffix("xcodeproj") } + let pbxprojFiles = xcodeprojFiles.map { $0 + "project.pbxproj" } + for pbxprojFile in pbxprojFiles { + FengNiao.deleteReference(projectFilePath: pbxprojFile, deletedFiles: deleted) + } + } + + DispatchQueue.main.async { + self.consoleStatus += "\nUnused reference delete successfully" + self.cancellable = nil + self.deleteAmount = 100 + } + } + } +} diff --git a/App/macOS/ExportView.swift b/App/macOS/ExportView.swift new file mode 100644 index 0000000..a13bbaf --- /dev/null +++ b/App/macOS/ExportView.swift @@ -0,0 +1,96 @@ +import SwiftUI +import FengNiaoKit +import Cocoa + +struct ExportView: View { + let unusedFiles: [FengNiaoKit.FileInfo] + + @Environment(\.dismiss) private var dismiss + @State private var copyButtonTitle: String = "Copy CSV" + @State private var downloadButtonTitle: String = "Download .csv" + @State private var csvContent: String = "" + @State private var isShowingSavePanel = false + + var body: some View { + VStack { + Text("Export CSV") + .font(.title2) + .bold() + HStack { + Button(copyButtonTitle) { + copyButtonTitle = "Copied!" + let pasteboard = NSPasteboard.general + pasteboard.declareTypes([.string], owner: nil) + pasteboard.setString(csvContent, forType: .string) + + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + copyButtonTitle = "Copy CSV" + } + } + + Button(downloadButtonTitle) { + downloadCSV() + downloadButtonTitle = "Downloaded!" + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + downloadButtonTitle = "Download .csv" + } + } + } + + TextEditor(text: $csvContent) + + HStack { + Spacer() + Button("Done") { + dismiss() + } + .buttonStyle(.borderedProminent) + .tint(Color.accentColor) + } + } + .padding() + .onAppear { + makeCSVContent() + } + } + + private func makeCSVContent() { + var csvText = "File Name,Size,Full Path\n" + for fileInfo in unusedFiles { + let row = "\(fileInfo.fileName),\(fileInfo.size.fn_readableSize),\(fileInfo.path.string)\n" + csvText.append(row) + } + self.csvContent = csvText + } + + private func downloadCSV() { + do { + guard let fileURL = getCSVFileURL() else { return } + try self.csvContent.write(to: fileURL, atomically: true, encoding: .utf8) + + isShowingSavePanel = true + } catch { + print("Error exporting CSV file: \(error.localizedDescription)") + } + } + + private func getCSVFileURL() -> URL? { + let savePanel = NSSavePanel() + savePanel.allowedContentTypes = [.commaSeparatedText] + savePanel.nameFieldStringValue = "export.csv" + savePanel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first + + guard savePanel.runModal() == .OK, let fileURL = savePanel.url else { + return nil + } + + return fileURL + } +} + +struct ExportView_Previews: PreviewProvider { + static var previews: some View { + ExportView(unusedFiles: []) + .frame(minWidth: 300, idealWidth: 300, minHeight: 300, idealHeight: 500) + } +} diff --git a/App/macOS/FengNiaoApp.swift b/App/macOS/FengNiaoApp.swift new file mode 100644 index 0000000..4e29a94 --- /dev/null +++ b/App/macOS/FengNiaoApp.swift @@ -0,0 +1,11 @@ +import SwiftUI + +@main +struct FengNiaoApp: App { + var body: some Scene { + WindowGroup { + AppView() + .frame(minWidth: 300, idealWidth: 650, minHeight: 500) + } + } +} diff --git a/App/macOS/Preview Content/Preview Assets.xcassets/Contents.json b/App/macOS/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/App/macOS/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Package.swift b/Package.swift index 780635e..a7190ac 100644 --- a/Package.swift +++ b/Package.swift @@ -6,6 +6,7 @@ let package = Package( name: "FengNiao", products: [ .executable(name: "FengNiao", targets: ["FengNiao"]), + .library(name: "FengNiaoKit", targets: ["FengNiaoKit"]) ], dependencies: [ .package(url: "https://github.com/onevcat/Rainbow.git", from: "3.1.1"), diff --git a/Package@swift-4.2.swift b/Package@swift-4.2.swift index cb789bf..194c6e0 100644 --- a/Package@swift-4.2.swift +++ b/Package@swift-4.2.swift @@ -6,6 +6,7 @@ let package = Package( name: "FengNiao", products: [ .executable(name: "FengNiao", targets: ["FengNiao"]), + .library(name: "FengNiaoKit", targets: ["FengNiaoKit"]) ], dependencies: [ .package(url: "https://github.com/onevcat/Rainbow.git", from: "3.1.1"), diff --git a/Package@swift-5.0.swift b/Package@swift-5.0.swift index ce36218..5d34261 100644 --- a/Package@swift-5.0.swift +++ b/Package@swift-5.0.swift @@ -6,7 +6,8 @@ let package = Package( name: "FengNiao", platforms: [.macOS(.v10_10)], products: [ - .executable(name: "FengNiao", targets: ["FengNiao"]) + .executable(name: "FengNiao", targets: ["FengNiao"]), + .library(name: "FengNiaoKit", targets: ["FengNiaoKit"]) ], dependencies: [ .package(url: "https://github.com/onevcat/Rainbow.git", from: "3.1.1"), diff --git a/Sources/FengNiaoKit/FengNiao.swift b/Sources/FengNiaoKit/FengNiao.swift index c07adda..ef7421b 100644 --- a/Sources/FengNiaoKit/FengNiao.swift +++ b/Sources/FengNiaoKit/FengNiao.swift @@ -60,7 +60,7 @@ public struct FileInfo { public let size: Int public let fileName: String - init(path: String) { + public init(path: String) { self.path = Path(path) self.size = self.path.size self.fileName = self.path.lastComponent @@ -129,7 +129,7 @@ public struct FengNiao { } // Return a failed list of deleting - static public func delete(_ unusedFiles: [FileInfo]) -> (deleted: [FileInfo], failed :[(FileInfo, Error)]) { + static public func delete(_ unusedFiles: [FileInfo]) -> (deleted: [FileInfo], failed: [(FileInfo, Error)]) { var deleted = [FileInfo]() var failed = [(FileInfo, Error)]() for file in unusedFiles { @@ -192,7 +192,7 @@ public struct FengNiao { // Skip the folders which suffix with a non-folder extension. // eg: We need to skip a folder with such name: /myfolder.png/ (although we should blame the one who named it) let filePath = Path(file) - if let ext = filePath.extension, filePath.isDirectory && nonDirExtensions.contains(ext) { + if let ext = filePath.extension, filePath.isDirectory && nonDirExtensions.contains(ext) { continue } @@ -301,6 +301,3 @@ extension String { } } } - - - diff --git a/Sources/FengNiaoKit/FileSearchRule.swift b/Sources/FengNiaoKit/FileSearchRule.swift index fd2cfd5..417ef15 100644 --- a/Sources/FengNiaoKit/FileSearchRule.swift +++ b/Sources/FengNiaoKit/FileSearchRule.swift @@ -89,4 +89,3 @@ struct PbxprojImageSearchRule: RegPatternSearchRule { let extensions: [String] let patterns = ["ASSETCATALOG_COMPILER_APPICON_NAME = \"?(.*?)\"?;", "ASSETCATALOG_COMPILER_COMPLICATION_NAME = \"?(.*?)\"?;"] } -