From 6a0e521bc26b8907394a32a9b9cca3416818b8b4 Mon Sep 17 00:00:00 2001 From: Ancil Maxwell Hoffman Date: Sun, 14 Sep 2025 11:57:38 +0200 Subject: [PATCH 1/5] changes to make build with XCode26 --- .claude/settings.local.json | 9 ++ Scripts/fix_libfido2_framework.sh | 25 ++++ Xcodes.xcodeproj/project.pbxproj | 139 +++++++++--------- .../xcshareddata/xcschemes/Xcodes.xcscheme | 2 +- ...robotsandpencils.XcodesApp.Helper.xcscheme | 2 +- Xcodes/Resources/Licenses.rtf | 29 +--- 6 files changed, 103 insertions(+), 103 deletions(-) create mode 100644 .claude/settings.local.json create mode 100755 Scripts/fix_libfido2_framework.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..87dcd153 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Read(//Users/genesis/Library/Developer/Xcode/DerivedData/Xcodes-fcqivmyigvmwinblrqeyzsbmntec/Build/Products/Debug/Xcodes.app/Contents/Frameworks/**)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/Scripts/fix_libfido2_framework.sh b/Scripts/fix_libfido2_framework.sh new file mode 100755 index 00000000..0a40bed9 --- /dev/null +++ b/Scripts/fix_libfido2_framework.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# Fix libfido2.framework structure for macOS validation +FRAMEWORK_PATH="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Contents/Frameworks/libfido2.framework" + +if [ -d "$FRAMEWORK_PATH" ] && [ -f "$FRAMEWORK_PATH/Info.plist" ] && [ ! -d "$FRAMEWORK_PATH/Versions" ]; then + echo "Fixing libfido2.framework bundle structure..." + + # Create proper bundle structure + mkdir -p "$FRAMEWORK_PATH/Versions/A/Resources" + + # Move files to proper locations + mv "$FRAMEWORK_PATH/Info.plist" "$FRAMEWORK_PATH/Versions/A/Resources/" + #mv "$FRAMEWORK_PATH/libfido2" "$FRAMEWORK_PATH/Versions/A/" + #if [ -f "$FRAMEWORK_PATH/LICENSE" ]; then + # mv "$FRAMEWORK_PATH/LICENSE" "$FRAMEWORK_PATH/Versions/A/" + #fi + + # Create symbolic links + ln -sf A "$FRAMEWORK_PATH/Versions/Current" + ln -sf Versions/Current/libfido2 "$FRAMEWORK_PATH/libfido2" + ln -sf Versions/Current/Resources "$FRAMEWORK_PATH/Resources" + + echo "libfido2.framework structure fixed" +fi diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 40a59328..f9ce36a3 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 90; objects = { /* Begin PBXBuildFile section */ @@ -164,35 +164,30 @@ /* Begin PBXCopyFilesBuildPhase section */ CA9FF8AC2595967A00E47BAF /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; - dstSubfolderSpec = 0; + dstSubfolder = None; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; CA9FF8BB259596B500E47BAF /* Copy Helper */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; dstPath = Contents/Library/LaunchServices; - dstSubfolderSpec = 1; + dstSubfolder = Wrapper; files = ( E8FA00542B5B109800769CE0 /* com.xcodesorg.xcodesapp.Helper in Copy Helper */, ); name = "Copy Helper"; - runOnlyForDeploymentPostprocessing = 0; }; CAA8589225A2B76F00ACF8C0 /* Copy aria2c */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; dstPath = ""; - dstSubfolderSpec = 6; + dstSubfolder = Executables; files = ( E8CBDB8927ADE32300B22292 /* unxip in Copy aria2c */, CAA8589325A2B77E00ACF8C0 /* aria2c in Copy aria2c */, ); name = "Copy aria2c"; - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ @@ -346,14 +341,11 @@ /* Begin PBXFrameworksBuildPhase section */ CA9FF8AB2595967A00E47BAF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; }; CAD2E79B2449574E00113D76 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; files = ( 15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */, 33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */, @@ -371,15 +363,12 @@ E84E4F572B335094003F3959 /* OrderedCollections in Frameworks */, E8F44A1E296B4CD7002D6592 /* Path in Frameworks */, ); - runOnlyForDeploymentPostprocessing = 0; }; CAD2E7B02449575100113D76 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; files = ( CAC28188259EE27200B8AB0B /* CombineExpectations in Frameworks */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ @@ -691,8 +680,6 @@ ); buildRules = ( ); - dependencies = ( - ); name = com.xcodesorg.xcodesapp.Helper; productName = com.robotsandpencils.XcodesApp.Helper; productReference = CA9FF8AE2595967A00E47BAF /* com.xcodesorg.xcodesapp.Helper */; @@ -762,8 +749,9 @@ CAD2E7962449574E00113D76 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1220; - LastUpgradeCheck = 1140; + LastUpgradeCheck = 2600; ORGANIZATIONNAME = "Robots and Pencils"; TargetAttributes = { CA9FF8AD2595967A00E47BAF = { @@ -779,7 +767,6 @@ }; }; buildConfigurationList = CAD2E7992449574E00113D76 /* Build configuration list for PBXProject "Xcodes" */; - compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -819,6 +806,7 @@ E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */, 33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */, ); + preferredProjectObjectVersion = 90; productRefGroup = CAD2E79F2449574E00113D76 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -833,7 +821,6 @@ /* Begin PBXResourcesBuildPhase section */ CAD2E79C2449574E00113D76 /* Resources */ = { isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; files = ( CAD2E7A92449575000113D76 /* Preview Assets.xcassets in Resources */, 9DD4FFCB2B13EC1800C974F1 /* Localizable.xcstrings in Resources */, @@ -841,47 +828,41 @@ CAA858DB25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz in Resources */, CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */, ); - runOnlyForDeploymentPostprocessing = 0; }; CAD2E7B12449575100113D76 /* Resources */ = { isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; files = ( CA452BC1259FDDFE0072DFA4 /* Stub-version.plist in Resources */, CA452BEB25A236500072DFA4 /* Stub-0.0.0.Info.plist in Resources */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ CA9FF8292594F33200E47BAF /* Generate Acknowledgements */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); inputPaths = ( "$(SRCROOT)/Xcodes.xcodeproj", "$(SRCROOT)/**/*.LICENSE", ); name = "Generate Acknowledgements"; - outputFileListPaths = ( - ); outputPaths = ( "$(SRCROOT)/Xcodes/Resources/Licenses.rtf", ); - runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd \"${SRCROOT}/Xcodes/AcknowledgementsGenerator\"\nxcrun -sdk macosx swift run AcknowledgementsGenerator \\\n -p \"${SRCROOT}/Xcodes.xcodeproj\" \\\n -o \"${SRCROOT}/Xcodes/Resources/Licenses.rtf\"\n"; + shellScript = ( + "cd \"${SRCROOT}/Xcodes/AcknowledgementsGenerator\"", + "xcrun -sdk macosx swift run AcknowledgementsGenerator \\", + " -p \"${SRCROOT}/Xcodes.xcodeproj\" \\", + " -o \"${SRCROOT}/Xcodes/Resources/Licenses.rtf\"", + "", + ); }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ CA9FF8AA2595967A00E47BAF /* Sources */ = { isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; files = ( CA9FF8D025959A9700E47BAF /* HelperXPCShared.swift in Sources */, CA42DD7325AEB04300BC0B0C /* Logger.swift in Sources */, @@ -890,11 +871,9 @@ CA9FF8B12595967A00E47BAF /* main.swift in Sources */, CA9FF8E025959BAA00E47BAF /* ConnectionVerifier.swift in Sources */, ); - runOnlyForDeploymentPostprocessing = 0; }; CAD2E79A2449574E00113D76 /* Sources */ = { isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; files = ( CA9FF8CF25959A9700E47BAF /* HelperXPCShared.swift in Sources */, CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */, @@ -996,18 +975,15 @@ CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */, CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */, ); - runOnlyForDeploymentPostprocessing = 0; }; CAD2E7AF2449575100113D76 /* Sources */ = { isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; files = ( CAC281E7259FA45A00B8AB0B /* Environment+Mock.swift in Sources */, CAC281E2259FA44600B8AB0B /* Bundle+XcodesTests.swift in Sources */, CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */, CAB3AB0E25BCA6C200BF1B04 /* AppStateTests.swift in Sources */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ @@ -1025,10 +1001,11 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - CA8FB635256E154800469DA5 /* Test */ = { + CA8FB635256E154800469DA5 /* Test configuration for PBXProject "Xcodes" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1052,6 +1029,7 @@ 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; @@ -1060,9 +1038,11 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1083,13 +1063,14 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Test; }; - CA8FB636256E154800469DA5 /* Test */ = { + CA8FB636256E154800469DA5 /* Test configuration for PBXNativeTarget "Xcodes" */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -1099,6 +1080,7 @@ CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 32; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; @@ -1118,14 +1100,14 @@ }; name = Test; }; - CA8FB637256E154800469DA5 /* Test */ = { + CA8FB637256E154800469DA5 /* Test configuration for PBXNativeTarget "XcodesTests" */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = XcodesTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1142,14 +1124,15 @@ }; name = Test; }; - CA9FF8B22595967A00E47BAF /* Debug */ = { + CA9FF8B22595967A00E47BAF /* Debug configuration for PBXNativeTarget "com.xcodesorg.xcodesapp.Helper" */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY; CODE_SIGN_STYLE = Automatic; CREATE_INFOPLIST_SECTION_IN_BINARY = YES; - DEVELOPMENT_TEAM = ZU6GR6B2FY; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 9NP473RSFG; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist"; MARKETING_VERSION = 2.0.0; @@ -1167,7 +1150,7 @@ }; name = Debug; }; - CA9FF8B32595967A00E47BAF /* Test */ = { + CA9FF8B32595967A00E47BAF /* Test configuration for PBXNativeTarget "com.xcodesorg.xcodesapp.Helper" */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; @@ -1175,6 +1158,7 @@ CODE_SIGN_ENTITLEMENTS = com.xcodesorg.xcodesapp.Helper/com.xcodesorg.xcodesapp.HelperTest.entitlements; CODE_SIGN_STYLE = Manual; CREATE_INFOPLIST_SECTION_IN_BINARY = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist"; @@ -1194,7 +1178,7 @@ }; name = Test; }; - CA9FF8B42595967A00E47BAF /* Release */ = { + CA9FF8B42595967A00E47BAF /* Release configuration for PBXNativeTarget "com.xcodesorg.xcodesapp.Helper" */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; @@ -1202,7 +1186,8 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CREATE_INFOPLIST_SECTION_IN_BINARY = YES; - DEVELOPMENT_TEAM = ZU6GR6B2FY; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 9NP473RSFG; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist"; MARKETING_VERSION = 2.0.0; @@ -1220,10 +1205,11 @@ }; name = Release; }; - CAD2E7BA2449575100113D76 /* Debug */ = { + CAD2E7BA2449575100113D76 /* Debug configuration for PBXProject "Xcodes" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1247,6 +1233,7 @@ 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; @@ -1255,9 +1242,11 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1278,16 +1267,18 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; - CAD2E7BB2449575100113D76 /* Release */ = { + CAD2E7BB2449575100113D76 /* Release configuration for PBXProject "Xcodes" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1311,6 +1302,7 @@ 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; @@ -1319,9 +1311,11 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -1335,13 +1329,14 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; - CAD2E7BD2449575100113D76 /* Debug */ = { + CAD2E7BD2449575100113D76 /* Debug configuration for PBXNativeTarget "Xcodes" */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -1351,8 +1346,9 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 32; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; - DEVELOPMENT_TEAM = ZU6GR6B2FY; + DEVELOPMENT_TEAM = 9NP473RSFG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Xcodes/Resources/Info.plist; @@ -1369,7 +1365,7 @@ }; name = Debug; }; - CAD2E7BE2449575100113D76 /* Release */ = { + CAD2E7BE2449575100113D76 /* Release configuration for PBXNativeTarget "Xcodes" */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -1379,8 +1375,9 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 32; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; - DEVELOPMENT_TEAM = ZU6GR6B2FY; + DEVELOPMENT_TEAM = 9NP473RSFG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Xcodes/Resources/Info.plist; @@ -1397,13 +1394,13 @@ }; name = Release; }; - CAD2E7C02449575100113D76 /* Debug */ = { + CAD2E7C02449575100113D76 /* Debug configuration for PBXNativeTarget "XcodesTests" */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ZU6GR6B2FY; INFOPLIST_FILE = XcodesTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1419,14 +1416,14 @@ }; name = Debug; }; - CAD2E7C12449575100113D76 /* Release */ = { + CAD2E7C12449575100113D76 /* Release configuration for PBXNativeTarget "XcodesTests" */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ZU6GR6B2FY; INFOPLIST_FILE = XcodesTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1448,41 +1445,37 @@ CA9FF8B52595967A00E47BAF /* Build configuration list for PBXNativeTarget "com.xcodesorg.xcodesapp.Helper" */ = { isa = XCConfigurationList; buildConfigurations = ( - CA9FF8B22595967A00E47BAF /* Debug */, - CA9FF8B32595967A00E47BAF /* Test */, - CA9FF8B42595967A00E47BAF /* Release */, + CA9FF8B22595967A00E47BAF /* Debug configuration for PBXNativeTarget "com.xcodesorg.xcodesapp.Helper" */, + CA9FF8B32595967A00E47BAF /* Test configuration for PBXNativeTarget "com.xcodesorg.xcodesapp.Helper" */, + CA9FF8B42595967A00E47BAF /* Release configuration for PBXNativeTarget "com.xcodesorg.xcodesapp.Helper" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CAD2E7992449574E00113D76 /* Build configuration list for PBXProject "Xcodes" */ = { isa = XCConfigurationList; buildConfigurations = ( - CAD2E7BA2449575100113D76 /* Debug */, - CA8FB635256E154800469DA5 /* Test */, - CAD2E7BB2449575100113D76 /* Release */, + CAD2E7BA2449575100113D76 /* Debug configuration for PBXProject "Xcodes" */, + CA8FB635256E154800469DA5 /* Test configuration for PBXProject "Xcodes" */, + CAD2E7BB2449575100113D76 /* Release configuration for PBXProject "Xcodes" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CAD2E7BC2449575100113D76 /* Build configuration list for PBXNativeTarget "Xcodes" */ = { isa = XCConfigurationList; buildConfigurations = ( - CAD2E7BD2449575100113D76 /* Debug */, - CA8FB636256E154800469DA5 /* Test */, - CAD2E7BE2449575100113D76 /* Release */, + CAD2E7BD2449575100113D76 /* Debug configuration for PBXNativeTarget "Xcodes" */, + CA8FB636256E154800469DA5 /* Test configuration for PBXNativeTarget "Xcodes" */, + CAD2E7BE2449575100113D76 /* Release configuration for PBXNativeTarget "Xcodes" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CAD2E7BF2449575100113D76 /* Build configuration list for PBXNativeTarget "XcodesTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - CAD2E7C02449575100113D76 /* Debug */, - CA8FB637256E154800469DA5 /* Test */, - CAD2E7C12449575100113D76 /* Release */, + CAD2E7C02449575100113D76 /* Debug configuration for PBXNativeTarget "XcodesTests" */, + CA8FB637256E154800469DA5 /* Test configuration for PBXNativeTarget "XcodesTests" */, + CAD2E7C12449575100113D76 /* Release configuration for PBXNativeTarget "XcodesTests" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ diff --git a/Xcodes.xcodeproj/xcshareddata/xcschemes/Xcodes.xcscheme b/Xcodes.xcodeproj/xcshareddata/xcschemes/Xcodes.xcscheme index f0379f7b..c26f7175 100644 --- a/Xcodes.xcodeproj/xcshareddata/xcschemes/Xcodes.xcscheme +++ b/Xcodes.xcodeproj/xcshareddata/xcschemes/Xcodes.xcscheme @@ -1,6 +1,6 @@ Date: Sun, 14 Sep 2025 12:32:16 +0200 Subject: [PATCH 2/5] Major and minor version collapsable lists implemented --- Xcodes.xcodeproj/project.pbxproj | 9 + .../xcshareddata/WorkspaceSettings.xcsettings | 5 + Xcodes/Backend/Xcode.swift | 94 ++++++++++- Xcodes/Frontend/XcodeList/XcodeListView.swift | 68 +++++++- .../XcodeList/XcodeMajorVersionRow.swift | 157 ++++++++++++++++++ .../XcodeList/XcodeMinorVersionRow.swift | 140 ++++++++++++++++ Xcodes/Resources/Localizable.xcstrings | 26 ++- 7 files changed, 490 insertions(+), 9 deletions(-) create mode 100644 Xcodes.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 Xcodes/Frontend/XcodeList/XcodeMajorVersionRow.swift create mode 100644 Xcodes/Frontend/XcodeList/XcodeMinorVersionRow.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index f9ce36a3..b9ea497a 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 05AB7F172E76C96E007C5CFE /* XcodeMajorVersionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05AB7F162E76C96E007C5CFE /* XcodeMajorVersionRow.swift */; }; + 05AB7F192E76CD8C007C5CFE /* XcodeMinorVersionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05AB7F182E76CD8C007C5CFE /* XcodeMinorVersionRow.swift */; }; 15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */; }; 33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 334A932B2CA885A400A5E079 /* LibFido2Swift */; }; 3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; }; @@ -192,6 +194,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 05AB7F162E76C96E007C5CFE /* XcodeMajorVersionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeMajorVersionRow.swift; sourceTree = ""; }; + 05AB7F182E76CD8C007C5CFE /* XcodeMinorVersionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeMinorVersionRow.swift; sourceTree = ""; }; 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; }; 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = ""; }; 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = ""; }; @@ -476,6 +480,8 @@ CA44901E2463AD34003D8213 /* Tag.swift */, CAE42486259A68A300B8B246 /* XcodeListCategory.swift */, CAD2E7A32449574E00113D76 /* XcodeListView.swift */, + 05AB7F162E76C96E007C5CFE /* XcodeMajorVersionRow.swift */, + 05AB7F182E76CD8C007C5CFE /* XcodeMinorVersionRow.swift */, CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */, E8D0296E284B029800647641 /* BottomStatusBar.swift */, ); @@ -856,6 +862,7 @@ " -p \"${SRCROOT}/Xcodes.xcodeproj\" \\", " -o \"${SRCROOT}/Xcodes/Resources/Licenses.rtf\"", "", + "", ); }; /* End PBXShellScriptBuildPhase section */ @@ -880,6 +887,7 @@ CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */, CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */, CAA8589B25A2B83000ACF8C0 /* Aria2CError.swift in Sources */, + 05AB7F192E76CD8C007C5CFE /* XcodeMinorVersionRow.swift in Sources */, 536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */, CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */, CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */, @@ -918,6 +926,7 @@ CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */, B0403CF02AD92D7B00137C09 /* ReleaseNotesView.swift in Sources */, CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */, + 05AB7F172E76C96E007C5CFE /* XcodeMajorVersionRow.swift in Sources */, CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */, E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */, CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */, diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/Xcodes/Backend/Xcode.swift b/Xcodes/Backend/Xcode.swift index b1721499..990137bd 100644 --- a/Xcodes/Backend/Xcode.swift +++ b/Xcodes/Backend/Xcode.swift @@ -85,5 +85,97 @@ struct Xcode: Identifiable, CustomStringConvertible { return nil } } - + +} + +struct XcodeMinorVersionGroup: Identifiable { + let majorVersion: Int + let minorVersion: Int + let versions: [Xcode] + var isExpanded: Bool = false + + var id: String { + "\(majorVersion).\(minorVersion)" + } + + var latestRelease: Xcode? { + versions + .filter { $0.version.isNotPrerelease } + .sorted { $0.version < $1.version } + .last + } + + var displayName: String { + "\(majorVersion).\(minorVersion)" + } + + var hasInstalled: Bool { + versions.contains { $0.installState.installed } + } + + var hasInstalling: Bool { + versions.contains { $0.installState.installing } + } + + var selectedVersion: Xcode? { + versions.first { $0.selected } + } +} + +struct XcodeMajorVersionGroup: Identifiable { + let majorVersion: Int + let minorVersionGroups: [XcodeMinorVersionGroup] + var isExpanded: Bool = false + + var id: Int { + majorVersion + } + + var versions: [Xcode] { + minorVersionGroups.flatMap { $0.versions } + } + + var latestRelease: Xcode? { + versions + .filter { $0.version.isNotPrerelease } + .sorted { $0.version < $1.version } + .last + } + + var displayName: String { + "\(majorVersion)" + } + + var hasInstalled: Bool { + minorVersionGroups.contains { $0.hasInstalled } + } + + var hasInstalling: Bool { + minorVersionGroups.contains { $0.hasInstalling } + } + + var selectedVersion: Xcode? { + minorVersionGroups.compactMap { $0.selectedVersion }.first + } +} + +extension Array where Element == Xcode { + func groupedByMajorVersion() -> [XcodeMajorVersionGroup] { + let majorGroups = Dictionary(grouping: self) { $0.version.major } + return majorGroups.map { majorVersion, xcodes in + let minorGroups = Dictionary(grouping: xcodes) { $0.version.minor } + let minorVersionGroups = minorGroups.map { minorVersion, minorXcodes in + XcodeMinorVersionGroup( + majorVersion: majorVersion, + minorVersion: minorVersion, + versions: minorXcodes.sorted { $0.version > $1.version } + ) + }.sorted { $0.minorVersion > $1.minorVersion } + + return XcodeMajorVersionGroup( + majorVersion: majorVersion, + minorVersionGroups: minorVersionGroups + ) + }.sorted { $0.majorVersion > $1.majorVersion } + } } diff --git a/Xcodes/Frontend/XcodeList/XcodeListView.swift b/Xcodes/Frontend/XcodeList/XcodeListView.swift index 9a8c9c41..24db0691 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListView.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListView.swift @@ -10,6 +10,8 @@ struct XcodeListView: View { private let architecture: XcodeListArchitecture private let isInstalledOnly: Bool @AppStorage(PreferenceKey.allowedMajorVersions.rawValue) private var allowedMajorVersions = Int.max + @State private var expandedMajorVersions = Set() + @State private var expandedMinorVersions = Set() init(selectedXcodeID: Binding, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool, architecture: XcodeListArchitecture) { self._selectedXcodeID = selectedXcodeID @@ -29,12 +31,12 @@ struct XcodeListView: View { case .beta: xcodes = appState.allXcodes.filter { $0.version.isPrerelease } } - + if architecture == .appleSilicon { xcodes = xcodes.filter { $0.architectures == [.arm64] } } - - + + let latestMajor = xcodes.sorted(\.version) .filter { $0.version.isNotPrerelease } .last? @@ -54,17 +56,69 @@ struct XcodeListView: View { if !searchText.isEmpty { xcodes = xcodes.filter { $0.description.contains(searchText) } } - + if isInstalledOnly { xcodes = xcodes.filter { $0.installState.installed } } - + return xcodes } + + var majorVersionGroups: [XcodeMajorVersionGroup] { + visibleXcodes.groupedByMajorVersion() + } var body: some View { - List(visibleXcodes, selection: $selectedXcodeID) { xcode in - XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id, appState: appState) + List(selection: $selectedXcodeID) { + ForEach(majorVersionGroups) { majorVersionGroup in + let isMajorExpanded = expandedMajorVersions.contains(majorVersionGroup.majorVersion) + + XcodeMajorVersionRow( + majorVersionGroup: majorVersionGroup, + isExpanded: isMajorExpanded, + onToggleExpanded: { + if isMajorExpanded { + expandedMajorVersions.remove(majorVersionGroup.majorVersion) + // Collapse all minor versions when major version is collapsed + for minorGroup in majorVersionGroup.minorVersionGroups { + expandedMinorVersions.remove(minorGroup.id) + } + } else { + expandedMajorVersions.insert(majorVersionGroup.majorVersion) + } + }, + appState: appState + ) + .tag(majorVersionGroup.selectedVersion?.id) + + if isMajorExpanded { + ForEach(majorVersionGroup.minorVersionGroups) { minorVersionGroup in + let isMinorExpanded = expandedMinorVersions.contains(minorVersionGroup.id) + + XcodeMinorVersionRow( + minorVersionGroup: minorVersionGroup, + isExpanded: isMinorExpanded, + onToggleExpanded: { + if isMinorExpanded { + expandedMinorVersions.remove(minorVersionGroup.id) + } else { + expandedMinorVersions.insert(minorVersionGroup.id) + } + }, + appState: appState + ) + .tag(minorVersionGroup.selectedVersion?.id) + + if isMinorExpanded { + ForEach(minorVersionGroup.versions) { xcode in + XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id, appState: appState) + .padding(.leading, 40) + .tag(xcode.id) + } + } + } + } + } } .listStyle(.sidebar) .safeAreaInset(edge: .bottom, spacing: 0) { diff --git a/Xcodes/Frontend/XcodeList/XcodeMajorVersionRow.swift b/Xcodes/Frontend/XcodeList/XcodeMajorVersionRow.swift new file mode 100644 index 00000000..8eadb37c --- /dev/null +++ b/Xcodes/Frontend/XcodeList/XcodeMajorVersionRow.swift @@ -0,0 +1,157 @@ +import SwiftUI +import Version +import Path + +struct XcodeMajorVersionRow: View { + let majorVersionGroup: XcodeMajorVersionGroup + let isExpanded: Bool + let onToggleExpanded: () -> Void + let appState: AppState + + var body: some View { + HStack { + Button(action: onToggleExpanded) { + HStack(spacing: 8) { + Image(systemName: isExpanded ? "chevron.down" : "chevron.right") + .font(.caption.weight(.semibold)) + .foregroundColor(.secondary) + + majorVersionIcon + + VStack(alignment: .leading, spacing: 2) { + Text("Xcode \(majorVersionGroup.displayName)") + .font(.body.weight(.medium)) + + if let latestRelease = majorVersionGroup.latestRelease { + Text("Latest: \(latestRelease.description)") + .font(.caption) + .foregroundColor(.secondary) + } + } + + Spacer() + } + } + .buttonStyle(.plain) + + selectControl() + .padding(.trailing, 16) + installControl() + } + .padding(.vertical, 8) + .background(Color.clear) + .contentShape(Rectangle()) + } + + @ViewBuilder + var majorVersionIcon: some View { + if let latestRelease = majorVersionGroup.latestRelease { + if let icon = latestRelease.icon { + Image(nsImage: icon) + .resizable() + .frame(width: 32, height: 32) + } else { + Image("xcode") + .resizable() + .frame(width: 32, height: 32) + .opacity(0.7) + } + } else { + Image("xcode-beta") + .resizable() + .frame(width: 32, height: 32) + .opacity(0.7) + } + } + + @ViewBuilder + func selectControl() -> some View { + if let selectedVersion = majorVersionGroup.selectedVersion { + if selectedVersion.selected { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + .help("ActiveVersionDescription") + } else { + EmptyView() + } + } else if majorVersionGroup.hasInstalled { + EmptyView() + } else { + EmptyView() + } + } + + @ViewBuilder + func installControl() -> some View { + if majorVersionGroup.hasInstalling { + if let installingVersion = majorVersionGroup.versions.first(where: { $0.installState.installing }) { + if case let .installing(installationStep) = installingVersion.installState { + InstallationStepRowView( + installationStep: installationStep, + highlighted: false, + cancel: { appState.presentedAlert = .cancelInstall(xcode: installingVersion) } + ) + } + } + } else if let latestRelease = majorVersionGroup.latestRelease { + switch latestRelease.installState { + case .installed: + Button("Open") { appState.open(xcode: latestRelease) } + .textCase(.uppercase) + .buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false)) + .help("OpenDescription") + case .notInstalled: + Button("Install Latest Release") { + appState.checkMinVersionAndInstall(id: latestRelease.id) + } + .textCase(.uppercase) + .buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false)) + .help("InstallLatestReleaseDescription") + case .installing: + EmptyView() + } + } + } +} + +struct XcodeMajorVersionRow_Previews: PreviewProvider { + static var previews: some View { + let sampleXcodes = [ + Xcode(version: Version("16.4.0")!, installState: .installed(Path("/Applications/Xcode-16.4.0.app")!), selected: true, icon: nil), + Xcode(version: Version("16.3.0")!, installState: .notInstalled, selected: false, icon: nil), + Xcode(version: Version("16.2.0")!, installState: .notInstalled, selected: false, icon: nil), + ] + + let minorVersionGroups = [ + XcodeMinorVersionGroup( + majorVersion: 16, + minorVersion: 4, + versions: [sampleXcodes[0]] + ), + XcodeMinorVersionGroup( + majorVersion: 16, + minorVersion: 3, + versions: [sampleXcodes[1]] + ), + XcodeMinorVersionGroup( + majorVersion: 16, + minorVersion: 2, + versions: [sampleXcodes[2]] + ) + ] + + let majorVersionGroup = XcodeMajorVersionGroup( + majorVersion: 16, + minorVersionGroups: minorVersionGroups, + isExpanded: false + ) + + XcodeMajorVersionRow( + majorVersionGroup: majorVersionGroup, + isExpanded: false, + onToggleExpanded: {}, + appState: AppState() + ) + .previewLayout(.sizeThatFits) + } +} diff --git a/Xcodes/Frontend/XcodeList/XcodeMinorVersionRow.swift b/Xcodes/Frontend/XcodeList/XcodeMinorVersionRow.swift new file mode 100644 index 00000000..be8a2b35 --- /dev/null +++ b/Xcodes/Frontend/XcodeList/XcodeMinorVersionRow.swift @@ -0,0 +1,140 @@ +import SwiftUI +import Version +import Path + +struct XcodeMinorVersionRow: View { + let minorVersionGroup: XcodeMinorVersionGroup + let isExpanded: Bool + let onToggleExpanded: () -> Void + let appState: AppState + + var body: some View { + HStack { + Button(action: onToggleExpanded) { + HStack(spacing: 8) { + Image(systemName: isExpanded ? "chevron.down" : "chevron.right") + .font(.caption.weight(.semibold)) + .foregroundColor(.secondary) + + minorVersionIcon + + VStack(alignment: .leading, spacing: 2) { + Text("Xcode \(minorVersionGroup.displayName)") + .font(.callout.weight(.medium)) + + if let latestRelease = minorVersionGroup.latestRelease { + Text("Latest: \(latestRelease.description)") + .font(.caption2) + .foregroundColor(.secondary) + } + } + + Spacer() + } + } + .buttonStyle(.plain) + + selectControl() + .padding(.trailing, 16) + installControl() + } + .padding(.vertical, 6) + .padding(.leading, 20) + .background(Color.clear) + .contentShape(Rectangle()) + } + + @ViewBuilder + var minorVersionIcon: some View { + if let latestRelease = minorVersionGroup.latestRelease { + if let icon = latestRelease.icon { + Image(nsImage: icon) + .resizable() + .frame(width: 28, height: 28) + } else { + Image("xcode") + .resizable() + .frame(width: 28, height: 28) + .opacity(0.6) + } + } else { + Image("xcode-beta") + .resizable() + .frame(width: 28, height: 28) + .opacity(0.6) + } + } + + @ViewBuilder + func selectControl() -> some View { + if let selectedVersion = minorVersionGroup.selectedVersion { + if selectedVersion.selected { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + .help("ActiveVersionDescription") + } else { + EmptyView() + } + } else if minorVersionGroup.hasInstalled { + EmptyView() + } else { + EmptyView() + } + } + + @ViewBuilder + func installControl() -> some View { + if minorVersionGroup.hasInstalling { + if let installingVersion = minorVersionGroup.versions.first(where: { $0.installState.installing }) { + if case let .installing(installationStep) = installingVersion.installState { + InstallationStepRowView( + installationStep: installationStep, + highlighted: false, + cancel: { appState.presentedAlert = .cancelInstall(xcode: installingVersion) } + ) + } + } + } else if let latestRelease = minorVersionGroup.latestRelease { + switch latestRelease.installState { + case .installed: + Button("Open") { appState.open(xcode: latestRelease) } + .textCase(.uppercase) + .buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false)) + .help("OpenDescription") + case .notInstalled: + Button("Install Latest") { + appState.checkMinVersionAndInstall(id: latestRelease.id) + } + .textCase(.uppercase) + .buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false)) + .help("InstallLatestVersionDescription") + case .installing: + EmptyView() + } + } + } +} + +struct XcodeMinorVersionRow_Previews: PreviewProvider { + static var previews: some View { + let sampleXcodes = [ + Xcode(version: Version("16.4.0")!, installState: .installed(Path("/Applications/Xcode-16.4.0.app")!), selected: true, icon: nil), + Xcode(version: Version("16.4.1")!, installState: .notInstalled, selected: false, icon: nil), + ] + + let minorVersionGroup = XcodeMinorVersionGroup( + majorVersion: 16, + minorVersion: 4, + versions: sampleXcodes, + isExpanded: false + ) + + XcodeMinorVersionRow( + minorVersionGroup: minorVersionGroup, + isExpanded: false, + onToggleExpanded: {}, + appState: AppState() + ) + .previewLayout(.sizeThatFits) + } +} \ No newline at end of file diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index 9d95df7a..df80e79d 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -10845,6 +10845,14 @@ } } }, + "Install Latest" : { + "comment" : "A button that installs the latest Xcode version.", + "isCommentAutoGenerated" : true + }, + "Install Latest Release" : { + "comment" : "A button that installs the latest release of Xcode.", + "isCommentAutoGenerated" : true + }, "Install Universal" : { "localizations" : { "ar" : { @@ -13587,6 +13595,14 @@ } } }, + "InstallLatestReleaseDescription" : { + "comment" : "A button label that instructs the user to install the latest release of Xcode.", + "isCommentAutoGenerated" : true + }, + "InstallLatestVersionDescription" : { + "comment" : "A button label that instructs the user to install the latest version of Xcode.", + "isCommentAutoGenerated" : true + }, "InstallPathDescription" : { "localizations" : { "ar" : { @@ -13836,6 +13852,10 @@ } } }, + "Latest: %@" : { + "comment" : "A sublabel within the Xcode major version row that shows the description of the latest release in that version group.", + "isCommentAutoGenerated" : true + }, "License" : { "localizations" : { "ar" : { @@ -23687,6 +23707,10 @@ } } }, + "Xcode %@" : { + "comment" : "A title and optional subtitle for a row in the checkout view, displaying the Xcode version and its latest release.", + "isCommentAutoGenerated" : true + }, "Xcodes" : { "localizations" : { "ar" : { @@ -23806,5 +23830,5 @@ } } }, - "version" : "1.0" + "version" : "1.1" } \ No newline at end of file From 395399bdd6823e73132d120ab84090e439a18b70 Mon Sep 17 00:00:00 2001 From: Ancil Maxwell Hoffman Date: Sun, 14 Sep 2025 13:20:43 +0200 Subject: [PATCH 3/5] added fix for libfido structure, completed testing --- Scripts/fix_libfido2_framework.sh | 9 ++++---- Xcodes.xcodeproj/project.pbxproj | 23 ++++++++++++++----- Xcodes/Frontend/XcodeList/XcodeListView.swift | 2 +- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/Scripts/fix_libfido2_framework.sh b/Scripts/fix_libfido2_framework.sh index 0a40bed9..d9111062 100755 --- a/Scripts/fix_libfido2_framework.sh +++ b/Scripts/fix_libfido2_framework.sh @@ -1,6 +1,7 @@ #!/bin/sh # Fix libfido2.framework structure for macOS validation +# If this script is not run, the build will fail because xcodebuild is expecting the library in a specific structure FRAMEWORK_PATH="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Contents/Frameworks/libfido2.framework" if [ -d "$FRAMEWORK_PATH" ] && [ -f "$FRAMEWORK_PATH/Info.plist" ] && [ ! -d "$FRAMEWORK_PATH/Versions" ]; then @@ -11,10 +12,10 @@ if [ -d "$FRAMEWORK_PATH" ] && [ -f "$FRAMEWORK_PATH/Info.plist" ] && [ ! -d "$F # Move files to proper locations mv "$FRAMEWORK_PATH/Info.plist" "$FRAMEWORK_PATH/Versions/A/Resources/" - #mv "$FRAMEWORK_PATH/libfido2" "$FRAMEWORK_PATH/Versions/A/" - #if [ -f "$FRAMEWORK_PATH/LICENSE" ]; then - # mv "$FRAMEWORK_PATH/LICENSE" "$FRAMEWORK_PATH/Versions/A/" - #fi + mv "$FRAMEWORK_PATH/libfido2" "$FRAMEWORK_PATH/Versions/A/" + if [ -f "$FRAMEWORK_PATH/LICENSE" ]; then + mv "$FRAMEWORK_PATH/LICENSE" "$FRAMEWORK_PATH/Versions/A/" + fi # Create symbolic links ln -sf A "$FRAMEWORK_PATH/Versions/Current" diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index b9ea497a..4064e96e 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -696,6 +696,7 @@ buildConfigurationList = CAD2E7BC2449575100113D76 /* Build configuration list for PBXNativeTarget "Xcodes" */; buildPhases = ( CAD2E79A2449574E00113D76 /* Sources */, + 05EACA532E76D21100CF1F9D /* Fix libfido2 structure */, CAD2E79B2449574E00113D76 /* Frameworks */, CA9FF8292594F33200E47BAF /* Generate Acknowledgements */, CAD2E79C2449574E00113D76 /* Resources */, @@ -845,6 +846,15 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 05EACA532E76D21100CF1F9D /* Fix libfido2 structure */ = { + isa = PBXShellScriptBuildPhase; + name = "Fix libfido2 structure"; + shellPath = /bin/sh; + shellScript = ( + "/Users/genesis/Coding/XCode/OpenSourceContributions/XcodesApp/Scripts/fix_libfido2_framework.sh", + "", + ); + }; CA9FF8292594F33200E47BAF /* Generate Acknowledgements */ = { isa = PBXShellScriptBuildPhase; inputPaths = ( @@ -863,6 +873,7 @@ " -o \"${SRCROOT}/Xcodes/Resources/Licenses.rtf\"", "", "", + "", ); }; /* End PBXShellScriptBuildPhase section */ @@ -1088,7 +1099,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; DEVELOPMENT_TEAM = ""; @@ -1101,7 +1112,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 3.0.0; + MARKETING_VERSION = 3.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp; PRODUCT_NAME = Xcodes; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1354,7 +1365,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; DEVELOPMENT_TEAM = 9NP473RSFG; @@ -1367,7 +1378,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 3.0.0; + MARKETING_VERSION = 3.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp; PRODUCT_NAME = Xcodes; SWIFT_VERSION = 5.0; @@ -1383,7 +1394,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; DEVELOPMENT_TEAM = 9NP473RSFG; @@ -1396,7 +1407,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 3.0.0; + MARKETING_VERSION = 3.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp; PRODUCT_NAME = Xcodes; SWIFT_VERSION = 5.0; diff --git a/Xcodes/Frontend/XcodeList/XcodeListView.swift b/Xcodes/Frontend/XcodeList/XcodeListView.swift index 24db0691..3704f38e 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListView.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListView.swift @@ -146,7 +146,7 @@ struct PlatformsPocket: View { .font(.body.weight(.medium)) .padding(.horizontal) .padding(.vertical, 12) - .background(.quaternary.opacity(0.75)) + .background(.quaternary.opacity(0.9)) .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) } .buttonStyle(.plain) From c68f7ce180ad628acf6458212f3c903ac59fb5df Mon Sep 17 00:00:00 2001 From: Ancil Maxwell Hoffman Date: Sun, 14 Sep 2025 13:35:18 +0200 Subject: [PATCH 4/5] removed claude files --- .claude/settings.local.json | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 87dcd153..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "permissions": { - "allow": [ - "Read(//Users/genesis/Library/Developer/Xcode/DerivedData/Xcodes-fcqivmyigvmwinblrqeyzsbmntec/Build/Products/Debug/Xcodes.app/Contents/Frameworks/**)" - ], - "deny": [], - "ask": [] - } -} \ No newline at end of file From 061baa568bc4d92aad4e0d712176261ec3cba8c4 Mon Sep 17 00:00:00 2001 From: Ancil Maxwell Hoffman Date: Sun, 21 Sep 2025 14:53:47 +0200 Subject: [PATCH 5/5] adjusted shellscript, implemented collapsablelist preference, changed Dev Team back, adjusted localisations --- Xcodes.xcodeproj/project.pbxproj | 21 ++- Xcodes/Backend/AppState.swift | 7 + .../Preferences/GeneralPreferencePane.swift | 1 + Xcodes/Frontend/XcodeList/XcodeListView.swift | 120 +++++++++++------- .../XcodeList/XcodeMajorVersionRow.swift | 4 +- .../XcodeList/XcodeMinorVersionRow.swift | 6 +- Xcodes/Resources/Localizable.xcstrings | 18 +++ 7 files changed, 120 insertions(+), 57 deletions(-) diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 4064e96e..e634c897 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -848,10 +848,12 @@ /* Begin PBXShellScriptBuildPhase section */ 05EACA532E76D21100CF1F9D /* Fix libfido2 structure */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; name = "Fix libfido2 structure"; shellPath = /bin/sh; shellScript = ( - "/Users/genesis/Coding/XCode/OpenSourceContributions/XcodesApp/Scripts/fix_libfido2_framework.sh", + "./Scripts/fix_libfido2_framework.sh", + "", "", ); }; @@ -874,6 +876,8 @@ "", "", "", + "", + "", ); }; /* End PBXShellScriptBuildPhase section */ @@ -1060,6 +1064,7 @@ COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -1102,7 +1107,7 @@ CURRENT_PROJECT_VERSION = 33; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_HARDENED_RUNTIME = NO; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Xcodes/Resources/Info.plist; @@ -1152,7 +1157,7 @@ CODE_SIGN_STYLE = Automatic; CREATE_INFOPLIST_SECTION_IN_BINARY = YES; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 9NP473RSFG; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist"; MARKETING_VERSION = 2.0.0; @@ -1179,7 +1184,7 @@ CODE_SIGN_STYLE = Manual; CREATE_INFOPLIST_SECTION_IN_BINARY = YES; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_HARDENED_RUNTIME = NO; INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist"; MARKETING_VERSION = 2.0.0; @@ -1207,7 +1212,7 @@ CODE_SIGN_STYLE = Automatic; CREATE_INFOPLIST_SECTION_IN_BINARY = YES; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 9NP473RSFG; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "$(SRCROOT)/$(TARGET_NAME)/Info.plist"; MARKETING_VERSION = 2.0.0; @@ -1264,6 +1269,7 @@ COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -1333,6 +1339,7 @@ COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -1368,7 +1375,7 @@ CURRENT_PROJECT_VERSION = 33; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; - DEVELOPMENT_TEAM = 9NP473RSFG; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Xcodes/Resources/Info.plist; @@ -1397,7 +1404,7 @@ CURRENT_PROJECT_VERSION = 33; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; - DEVELOPMENT_TEAM = 9NP473RSFG; + DEVELOPMENT_TEAM = ZU6GR6B2FY; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Xcodes/Resources/Info.plist; diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 1f6419dc..1b803901 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -139,6 +139,12 @@ class AppState: ObservableObject { } } + @Published var collapsableListEnabled = true { + didSet { + Current.defaults.set(collapsableListEnabled, forKey: "collapsableListEnabled") + } + } + // MARK: - Runtimes @Published var downloadableRuntimes: [DownloadableRuntime] = [] @@ -209,6 +215,7 @@ class AppState: ObservableObject { installPath = Current.defaults.string(forKey: "installPath") ?? Path.defaultInstallDirectory.string showOpenInRosettaOption = Current.defaults.bool(forKey: "showOpenInRosettaOption") ?? false terminateAfterLastWindowClosed = Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false + collapsableListEnabled = Current.defaults.bool(forKey: "collapsibleListEnabled") ?? true } // MARK: Timer diff --git a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift index b15f5c6d..de4f3b83 100644 --- a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift @@ -24,6 +24,7 @@ struct GeneralPreferencePane: View { GroupBox(label: Text("Misc")) { Toggle("TerminateAfterLastWindowClosed", isOn: $appState.terminateAfterLastWindowClosed) + Toggle("EnableCollapsibleList", isOn: $appState.collapsableListEnabled) } .groupBoxStyle(PreferencesGroupBoxStyle()) } diff --git a/Xcodes/Frontend/XcodeList/XcodeListView.swift b/Xcodes/Frontend/XcodeList/XcodeListView.swift index 3704f38e..2eb64069 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListView.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListView.swift @@ -70,53 +70,16 @@ struct XcodeListView: View { var body: some View { List(selection: $selectedXcodeID) { - ForEach(majorVersionGroups) { majorVersionGroup in - let isMajorExpanded = expandedMajorVersions.contains(majorVersionGroup.majorVersion) - - XcodeMajorVersionRow( - majorVersionGroup: majorVersionGroup, - isExpanded: isMajorExpanded, - onToggleExpanded: { - if isMajorExpanded { - expandedMajorVersions.remove(majorVersionGroup.majorVersion) - // Collapse all minor versions when major version is collapsed - for minorGroup in majorVersionGroup.minorVersionGroups { - expandedMinorVersions.remove(minorGroup.id) - } - } else { - expandedMajorVersions.insert(majorVersionGroup.majorVersion) - } - }, + if appState.collapsableListEnabled { + CollapsableListView( + visibleXcodes: visibleXcodes, + selectedXcodeID: $selectedXcodeID, appState: appState ) - .tag(majorVersionGroup.selectedVersion?.id) - - if isMajorExpanded { - ForEach(majorVersionGroup.minorVersionGroups) { minorVersionGroup in - let isMinorExpanded = expandedMinorVersions.contains(minorVersionGroup.id) - - XcodeMinorVersionRow( - minorVersionGroup: minorVersionGroup, - isExpanded: isMinorExpanded, - onToggleExpanded: { - if isMinorExpanded { - expandedMinorVersions.remove(minorVersionGroup.id) - } else { - expandedMinorVersions.insert(minorVersionGroup.id) - } - }, - appState: appState - ) - .tag(minorVersionGroup.selectedVersion?.id) - - if isMinorExpanded { - ForEach(minorVersionGroup.versions) { xcode in - XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id, appState: appState) - .padding(.leading, 40) - .tag(xcode.id) - } - } - } + } else { + ForEach(visibleXcodes) { xcode in + XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id, appState: appState) + .tag(xcode.id) } } } @@ -129,6 +92,72 @@ struct XcodeListView: View { } } +struct CollapsableListView: View { + let visibleXcodes: [Xcode] + @Binding var selectedXcodeID: Xcode.ID? + let appState: AppState + + @State private var expandedMajorVersions = Set() + @State private var expandedMinorVersions = Set() + + var majorVersionGroups: [XcodeMajorVersionGroup] { + visibleXcodes.groupedByMajorVersion() + } + + var body: some View { + ForEach(majorVersionGroups) { majorVersionGroup in + let isMajorExpanded = expandedMajorVersions.contains(majorVersionGroup.majorVersion) + + XcodeMajorVersionRow( + majorVersionGroup: majorVersionGroup, + isExpanded: isMajorExpanded, + onToggleExpanded: { + if isMajorExpanded { + expandedMajorVersions.remove(majorVersionGroup.majorVersion) + // Collapse all minor versions when major version is collapsed + for minorGroup in majorVersionGroup.minorVersionGroups { + expandedMinorVersions.remove(minorGroup.id) + } + } else { + expandedMajorVersions.insert(majorVersionGroup.majorVersion) + } + }, + appState: appState + ) + .tag(majorVersionGroup.selectedVersion?.id) + + if isMajorExpanded { + ForEach(majorVersionGroup.minorVersionGroups) { minorVersionGroup in + let isMinorExpanded = expandedMinorVersions.contains(minorVersionGroup.id) + + XcodeMinorVersionRow( + minorVersionGroup: minorVersionGroup, + isExpanded: isMinorExpanded, + onToggleExpanded: { + if isMinorExpanded { + expandedMinorVersions.remove(minorVersionGroup.id) + } else { + expandedMinorVersions.insert(minorVersionGroup.id) + } + }, + appState: appState + ) + .tag(minorVersionGroup.selectedVersion?.id) + + if isMinorExpanded { + ForEach(minorVersionGroup.versions) { xcode in + XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id, appState: appState) + .padding(.leading, 40) + .tag(xcode.id) + } + } + } + } + } + } +} + + struct PlatformsPocket: View { @SwiftUI.Environment(\.openWindow) private var openWindow @@ -175,3 +204,4 @@ struct XcodeListView_Previews: PreviewProvider { .previewLayout(.sizeThatFits) } } + diff --git a/Xcodes/Frontend/XcodeList/XcodeMajorVersionRow.swift b/Xcodes/Frontend/XcodeList/XcodeMajorVersionRow.swift index 8eadb37c..60084176 100644 --- a/Xcodes/Frontend/XcodeList/XcodeMajorVersionRow.swift +++ b/Xcodes/Frontend/XcodeList/XcodeMajorVersionRow.swift @@ -19,11 +19,11 @@ struct XcodeMajorVersionRow: View { majorVersionIcon VStack(alignment: .leading, spacing: 2) { - Text("Xcode \(majorVersionGroup.displayName)") + Text(String(format: localizeString("Xcode %@"), majorVersionGroup.displayName)) .font(.body.weight(.medium)) if let latestRelease = majorVersionGroup.latestRelease { - Text("Latest: \(latestRelease.description)") + Text(String(format: localizeString("Latest: %@"), latestRelease.description)) .font(.caption) .foregroundColor(.secondary) } diff --git a/Xcodes/Frontend/XcodeList/XcodeMinorVersionRow.swift b/Xcodes/Frontend/XcodeList/XcodeMinorVersionRow.swift index be8a2b35..c75ca21f 100644 --- a/Xcodes/Frontend/XcodeList/XcodeMinorVersionRow.swift +++ b/Xcodes/Frontend/XcodeList/XcodeMinorVersionRow.swift @@ -19,11 +19,11 @@ struct XcodeMinorVersionRow: View { minorVersionIcon VStack(alignment: .leading, spacing: 2) { - Text("Xcode \(minorVersionGroup.displayName)") + Text(String(format: localizeString("Xcode %@"), minorVersionGroup.displayName)) .font(.callout.weight(.medium)) if let latestRelease = minorVersionGroup.latestRelease { - Text("Latest: \(latestRelease.description)") + Text(String(format: localizeString("Latest: %@"), latestRelease.description)) .font(.caption2) .foregroundColor(.secondary) } @@ -137,4 +137,4 @@ struct XcodeMinorVersionRow_Previews: PreviewProvider { ) .previewLayout(.sizeThatFits) } -} \ No newline at end of file +} diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index df80e79d..772f8743 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -8380,6 +8380,24 @@ } } }, + "EnableCollapsibleList" : { + "comment" : "A toggle that enables or disables the collapsible list feature.", + "isCommentAutoGenerated" : true, + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haupt- und Unterversionen in der Listenansicht zusammenklappen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Collapse Major and Minor Versions in the list view" + } + } + } + }, "EnableNotifications" : { "localizations" : { "ar" : {