mirror of
https://github.com/TuringSoftware/CrystalFetch.git
synced 2026-01-01 01:45:47 +08:00
Compare commits
61 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05e0b201da | ||
|
|
c0946e8781 | ||
|
|
8c801a857c | ||
|
|
30f426038a | ||
|
|
6a462109c3 | ||
|
|
53dc56900f | ||
|
|
b08cbda85e | ||
|
|
39069074b4 | ||
|
|
2dce2ff2c7 | ||
|
|
4f444d88c6 | ||
|
|
effd4aeda5 | ||
|
|
38f64ea283 | ||
|
|
7e51b9b419 | ||
|
|
e1e96ebf31 | ||
|
|
e03f718222 | ||
|
|
9761f74713 | ||
|
|
ca4e24700a | ||
|
|
de31fe1cc7 | ||
|
|
c55b261cec | ||
|
|
6a313e0658 | ||
|
|
70460147f7 | ||
|
|
81d3bc7c9c | ||
|
|
1b93e42ce7 | ||
|
|
010270f03f | ||
|
|
8206aa1567 | ||
|
|
baa045eff3 | ||
|
|
97fa4cc048 | ||
|
|
dd91aa3ccf | ||
|
|
77e4d71f69 | ||
|
|
d0fbf0fe66 | ||
|
|
2d4b1bd4ec | ||
|
|
5de7a87231 | ||
|
|
cc4f295b59 | ||
|
|
f0c2926081 | ||
|
|
a7368fdd31 | ||
|
|
8482fec6be | ||
|
|
d76d953697 | ||
|
|
5664c1bd67 | ||
|
|
604859d17a | ||
|
|
861a1fbe3d | ||
|
|
ae7d8b1379 | ||
|
|
1259bc3057 | ||
|
|
7225052413 | ||
|
|
65d00bfff6 | ||
|
|
b287caeeec | ||
|
|
7c6efa9653 | ||
|
|
b8c9faa6cd | ||
|
|
b25ad06dc3 | ||
|
|
9b00a0fdbb | ||
|
|
733f9ae7b8 | ||
|
|
5086227d62 | ||
|
|
3a2e830d6e | ||
|
|
5f37338911 | ||
|
|
c2a53cb03a | ||
|
|
449544ef37 | ||
|
|
c2a27275fa | ||
|
|
0b7c12c138 | ||
|
|
38434ac286 | ||
|
|
1c9cd05849 | ||
|
|
c19871b3da | ||
|
|
ad1247282b |
30 changed files with 2327 additions and 46 deletions
52
.github/workflows/build.yml
vendored
52
.github/workflows/build.yml
vendored
|
|
@ -20,12 +20,12 @@ on:
|
|||
|
||||
env:
|
||||
PRODUCT_NAME: CrystalFetch
|
||||
BUILD_XCODE_PATH: /Applications/Xcode_14.2.app
|
||||
BUILD_XCODE_PATH: /Applications/Xcode_16.2.app
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: macos-12
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
|
@ -37,19 +37,19 @@ jobs:
|
|||
[[ "$(xcode-select -p)" == "${{ env.BUILD_XCODE_PATH }}"* ]] || sudo xcode-select -s "${{ env.BUILD_XCODE_PATH }}"
|
||||
- name: Build
|
||||
run: |
|
||||
xcodebuild archive -archivePath "$PRODUCT_NAME" -scheme "$PRODUCT_NAME" -configuration Release CODE_SIGN_IDENTITY="-" PRODUCT_BUNDLE_PREFIX="$PRODUCT_BUNDLE_PREFIX"
|
||||
xcodebuild archive -archivePath "$PRODUCT_NAME" -scheme "$PRODUCT_NAME" -configuration Release CODE_SIGN_IDENTITY="-" PRODUCT_BUNDLE_PREFIX="$PRODUCT_BUNDLE_PREFIX" ONLY_ACTIVE_ARCH=No
|
||||
tar -acf $PRODUCT_NAME.xcarchive.tgz $PRODUCT_NAME.xcarchive
|
||||
env:
|
||||
PRODUCT_NAME: ${{ env.PRODUCT_NAME }}
|
||||
PRODUCT_BUNDLE_PREFIX: ${{ vars.PRODUCT_BUNDLE_PREFIX }}
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PRODUCT_NAME }}
|
||||
path: ${{ env.PRODUCT_NAME }}.xcarchive.tgz
|
||||
package:
|
||||
name: Package
|
||||
runs-on: macos-12
|
||||
runs-on: macos-15
|
||||
needs: [build]
|
||||
if: github.event_name == 'release' || github.event.inputs.test_release == 'true'
|
||||
steps:
|
||||
|
|
@ -60,6 +60,13 @@ jobs:
|
|||
with:
|
||||
p12-file-base64: ${{ secrets.SIGNING_CERTIFICATE_P12_DATA }}
|
||||
p12-password: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }}
|
||||
- name: Import App Store Connect API Key
|
||||
run: |
|
||||
mkdir -p ~/.appstoreconnect/private_keys
|
||||
echo $AUTHKEY_API_KEY | base64 --decode -o ~/.appstoreconnect/private_keys/AuthKey_$API_KEY.p8
|
||||
env:
|
||||
AUTHKEY_API_KEY: ${{ secrets.CONNECT_KEY }}
|
||||
API_KEY: ${{ vars.CONNECT_KEY_ID }}
|
||||
- name: Install Provisioning Profiles
|
||||
run: |
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
|
|
@ -68,7 +75,7 @@ jobs:
|
|||
PROFILE_DATA: ${{ vars.PROFILE_DATA }}
|
||||
PROFILE_UUID: ${{ vars.PROFILE_UUID }}
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PRODUCT_NAME }}
|
||||
- name: Package for Release
|
||||
|
|
@ -80,14 +87,16 @@ jobs:
|
|||
SIGNING_TEAM_ID: ${{ vars.SIGNING_TEAM_ID }}
|
||||
PROFILE_UUID: ${{ vars.PROFILE_UUID }}
|
||||
- name: Notarize app
|
||||
run: npx notarize-cli --file "$PRODUCT_NAME.dmg" --bundle-id "$BUNDLE_ID"
|
||||
run: |
|
||||
xcrun notarytool submit --issuer "$ISSUER_UUID" --key-id "$API_KEY" --key "~/.appstoreconnect/private_keys/AuthKey_$API_KEY.p8" --team-id "$SIGNING_TEAM_ID" --wait "$PRODUCT_NAME.dmg"
|
||||
xcrun stapler staple "$PRODUCT_NAME.dmg"
|
||||
env:
|
||||
BUNDLE_ID: ${{ vars.PRODUCT_BUNDLE_PREFIX }}.${{ env.PRODUCT_NAME }}
|
||||
NOTARIZE_USERNAME: ${{ secrets.SIGNING_USERNAME }}
|
||||
NOTARIZE_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||
SIGNING_TEAM_ID: ${{ vars.SIGNING_TEAM_ID }}
|
||||
ISSUER_UUID: ${{ vars.CONNECT_ISSUER_ID }}
|
||||
API_KEY: ${{ vars.CONNECT_KEY_ID }}
|
||||
- name: Upload Artifact
|
||||
if: github.event_name != 'release'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PRODUCT_NAME }}-dmg
|
||||
path: ${{ env.PRODUCT_NAME }}.dmg
|
||||
|
|
@ -103,17 +112,24 @@ jobs:
|
|||
asset_content_type: application/octet-stream
|
||||
submit:
|
||||
name: Submit
|
||||
runs-on: macos-12
|
||||
runs-on: macos-15
|
||||
needs: [build]
|
||||
if: github.event_name == 'release' || github.event.inputs.test_release == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Import signing certificate into keychain
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.SIGNING_CERTIFICATE_P12_DATA }}
|
||||
p12-password: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }}
|
||||
- name: Import App Store Connect API Key
|
||||
run: |
|
||||
mkdir -p ~/.appstoreconnect/private_keys
|
||||
echo $AUTHKEY_API_KEY | base64 --decode -o ~/.appstoreconnect/private_keys/AuthKey_$API_KEY.p8
|
||||
env:
|
||||
AUTHKEY_API_KEY: ${{ secrets.CONNECT_KEY }}
|
||||
API_KEY: ${{ vars.CONNECT_KEY_ID }}
|
||||
- name: Install Provisioning Profiles
|
||||
run: |
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
|
|
@ -122,7 +138,7 @@ jobs:
|
|||
PROFILE_DATA: ${{ vars.APP_STORE_PROFILE_DATA }}
|
||||
PROFILE_UUID: ${{ vars.APP_STORE_PROFILE_UUID }}
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PRODUCT_NAME }}
|
||||
- name: Package for App Store
|
||||
|
|
@ -135,15 +151,15 @@ jobs:
|
|||
PROFILE_UUID: ${{ vars.APP_STORE_PROFILE_UUID }}
|
||||
- name: Upload Artifact
|
||||
if: github.event_name != 'release'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PRODUCT_NAME }}-pkg
|
||||
path: ${{ env.PRODUCT_NAME }}.pkg
|
||||
- name: Upload app to App Store Connect
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
xcrun altool --upload-app -t macos -f "$PRODUCT_NAME.pkg" -u "$SUBMIT_USERNAME" -p "$SUBMIT_PASSWORD"
|
||||
xcrun altool --upload-app -t macos -f "$PRODUCT_NAME.pkg" --apiKey "$API_KEY" --apiIssuer "$ISSUER_UUID"
|
||||
env:
|
||||
PRODUCT_NAME: ${{ env.PRODUCT_NAME }}
|
||||
SUBMIT_USERNAME: ${{ secrets.SIGNING_USERNAME }}
|
||||
SUBMIT_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||
ISSUER_UUID: ${{ vars.CONNECT_ISSUER_ID }}
|
||||
API_KEY: ${{ vars.CONNECT_KEY_ID }}
|
||||
|
|
|
|||
2
.gitmodules
vendored
2
.gitmodules
vendored
|
|
@ -12,7 +12,7 @@
|
|||
url = https://github.com/ebiggers/wimlib
|
||||
[submodule "converter"]
|
||||
path = converter
|
||||
url = https://github.com/uup-dump/converter.git
|
||||
url = https://git.uupdump.net/uup-dump/converter.git
|
||||
[submodule "OpenSSL"]
|
||||
path = OpenSSL
|
||||
url = https://github.com/krzyzanowskim/OpenSSL.git
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
MARKETING_VERSION = 1.0.0
|
||||
CURRENT_PROJECT_VERSION = 1
|
||||
MARKETING_VERSION = 2.2.0
|
||||
CURRENT_PROJECT_VERSION = 6
|
||||
|
||||
#include? "CodeSigning.xcconfig"
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
DEVELOPMENT_TEAM = WDNLXAD4W8
|
||||
|
|
@ -7,6 +7,12 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
844A8EFB2A860F91009A389C /* ESDCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844A8EFA2A860F91009A389C /* ESDCatalog.swift */; };
|
||||
844A8EFF2A86CA8C009A389C /* SimpleContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844A8EFE2A86CA8C009A389C /* SimpleContentView.swift */; };
|
||||
844A8F032A86E86F009A389C /* EULAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844A8F022A86E86F009A389C /* EULAView.swift */; };
|
||||
844A8F062A86F92F009A389C /* esd2iso.sh in Resources */ = {isa = PBXBuildFile; fileRef = 844A8F052A86F92F009A389C /* esd2iso.sh */; };
|
||||
84EB35722A870EA7004F252E /* ShowWindowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EB35712A870EA6004F252E /* ShowWindowButtonView.swift */; };
|
||||
CE39C8A22D97003600A83CE8 /* MCTCatalogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE39C8A12D97003600A83CE8 /* MCTCatalogs.swift */; };
|
||||
CEC09F0F2A6BB66200980857 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC09F0E2A6BB66200980857 /* Main.swift */; };
|
||||
CEC09F112A6BB66200980857 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC09F102A6BB66200980857 /* ContentView.swift */; };
|
||||
CEC09F132A6BB66300980857 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CEC09F122A6BB66300980857 /* Assets.xcassets */; };
|
||||
|
|
@ -360,6 +366,8 @@
|
|||
CEC0A2FC2A70A48200980857 /* aria2c in Copy Executables */ = {isa = PBXBuildFile; fileRef = CEC0A2F32A70A43100980857 /* aria2c */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
CEC0A3002A70A6CD00980857 /* convert.sh in Copy Scripts */ = {isa = PBXBuildFile; fileRef = CEC0A2FE2A70A6CD00980857 /* convert.sh */; };
|
||||
CEC0A3012A70A6CD00980857 /* convert_ve_plugin in Copy Scripts */ = {isa = PBXBuildFile; fileRef = CEC0A2FF2A70A6CD00980857 /* convert_ve_plugin */; };
|
||||
CEC8BC502A7B55D80042878F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CEC8BC522A7B55D80042878F /* Localizable.strings */; };
|
||||
FFB1B52A2A929CEC00B95D56 /* CrystalFetch-InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFB1B5282A929CEC00B95D56 /* CrystalFetch-InfoPlist.strings */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -487,6 +495,12 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
844A8EFA2A860F91009A389C /* ESDCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESDCatalog.swift; sourceTree = "<group>"; };
|
||||
844A8EFE2A86CA8C009A389C /* SimpleContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleContentView.swift; sourceTree = "<group>"; };
|
||||
844A8F022A86E86F009A389C /* EULAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EULAView.swift; sourceTree = "<group>"; };
|
||||
844A8F052A86F92F009A389C /* esd2iso.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = esd2iso.sh; sourceTree = "<group>"; };
|
||||
84EB35712A870EA6004F252E /* ShowWindowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowWindowButtonView.swift; sourceTree = "<group>"; };
|
||||
CE39C8A12D97003600A83CE8 /* MCTCatalogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCTCatalogs.swift; sourceTree = "<group>"; };
|
||||
CEC09F0B2A6BB66200980857 /* CrystalFetch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CrystalFetch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CEC09F0E2A6BB66200980857 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Main.swift; sourceTree = "<group>"; };
|
||||
CEC09F102A6BB66200980857 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -841,6 +855,12 @@
|
|||
CEC0A2FE2A70A6CD00980857 /* convert.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = convert.sh; sourceTree = "<group>"; };
|
||||
CEC0A2FF2A70A6CD00980857 /* convert_ve_plugin */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = convert_ve_plugin; sourceTree = "<group>"; };
|
||||
CEC0A3082A71BBA900980857 /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = "<group>"; };
|
||||
CEC8BC512A7B55D80042878F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
CEC8BC532A7B56280042878F /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
F65FBD842AA77422007637B0 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
F65FBD852AA77426007637B0 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
FFB1B5272A929A2800B95D56 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
FFB1B5292A929CEC00B95D56 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/CrystalFetch-InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -893,11 +913,29 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
844A8EF92A860DF6009A389C /* ESDCatalog */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
844A8EFA2A860F91009A389C /* ESDCatalog.swift */,
|
||||
CE39C8A12D97003600A83CE8 /* MCTCatalogs.swift */,
|
||||
);
|
||||
path = ESDCatalog;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
844A8F042A86F911009A389C /* Extras */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
844A8F052A86F92F009A389C /* esd2iso.sh */,
|
||||
);
|
||||
path = Extras;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEC09F022A6BB66200980857 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CEC0A3082A71BBA900980857 /* Build.xcconfig */,
|
||||
CEC09F0D2A6BB66200980857 /* Source */,
|
||||
844A8F042A86F911009A389C /* Extras */,
|
||||
CEC0A3022A70A6D500980857 /* converter */,
|
||||
CEC09F442A6F645400980857 /* cabextract */,
|
||||
CEC09FD42A6F771C00980857 /* mkisofs */,
|
||||
|
|
@ -925,18 +963,24 @@
|
|||
CEC09F0D2A6BB66200980857 /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
844A8EF92A860DF6009A389C /* ESDCatalog */,
|
||||
CEC09F292A6DC37800980857 /* UUPDump */,
|
||||
CEC09F0E2A6BB66200980857 /* Main.swift */,
|
||||
CEC09F102A6BB66200980857 /* ContentView.swift */,
|
||||
844A8EFE2A86CA8C009A389C /* SimpleContentView.swift */,
|
||||
CEC09F3C2A6EECC700980857 /* Downloader.swift */,
|
||||
844A8F022A86E86F009A389C /* EULAView.swift */,
|
||||
CEC09F252A6DAB7E00980857 /* BuildConfigView.swift */,
|
||||
CEC09F362A6E5B7600980857 /* BuildDetails.swift */,
|
||||
CEC09F382A6E5D5C00980857 /* BuildEditions.swift */,
|
||||
CEC09F342A6DF33C00980857 /* BuildsListView.swift */,
|
||||
84EB35712A870EA6004F252E /* ShowWindowButtonView.swift */,
|
||||
CEC09F272A6DBED400980857 /* Worker.swift */,
|
||||
CEC09F122A6BB66300980857 /* Assets.xcassets */,
|
||||
CEC09F172A6BB66300980857 /* CrystalFetch.entitlements */,
|
||||
CEC09F3E2A6F151800980857 /* CrystalFetch-Info.plist */,
|
||||
FFB1B5282A929CEC00B95D56 /* CrystalFetch-InfoPlist.strings */,
|
||||
CEC8BC522A7B55D80042878F /* Localizable.strings */,
|
||||
);
|
||||
path = Source;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1506,6 +1550,10 @@
|
|||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
ja,
|
||||
"zh-Hant",
|
||||
"zh-Hans",
|
||||
"zh-HK",
|
||||
);
|
||||
mainGroup = CEC09F022A6BB66200980857;
|
||||
packageReferences = (
|
||||
|
|
@ -1529,7 +1577,10 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
844A8F062A86F92F009A389C /* esd2iso.sh in Resources */,
|
||||
CEC09F132A6BB66300980857 /* Assets.xcassets in Resources */,
|
||||
CEC8BC502A7B55D80042878F /* Localizable.strings in Resources */,
|
||||
FFB1B52A2A929CEC00B95D56 /* CrystalFetch-InfoPlist.strings in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -1540,10 +1591,13 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE39C8A22D97003600A83CE8 /* MCTCatalogs.swift in Sources */,
|
||||
CEC09F112A6BB66200980857 /* ContentView.swift in Sources */,
|
||||
CEC09F312A6DC8FB00980857 /* UUPPackage.swift in Sources */,
|
||||
844A8EFF2A86CA8C009A389C /* SimpleContentView.swift in Sources */,
|
||||
CEC09F332A6DCA2C00980857 /* UUPDumpAPI.swift in Sources */,
|
||||
CEC09F282A6DBED400980857 /* Worker.swift in Sources */,
|
||||
844A8EFB2A860F91009A389C /* ESDCatalog.swift in Sources */,
|
||||
CEC09F262A6DAB7E00980857 /* BuildConfigView.swift in Sources */,
|
||||
CEC09F0F2A6BB66200980857 /* Main.swift in Sources */,
|
||||
CEC09F372A6E5B7600980857 /* BuildDetails.swift in Sources */,
|
||||
|
|
@ -1551,7 +1605,9 @@
|
|||
CEC09F3D2A6EECC700980857 /* Downloader.swift in Sources */,
|
||||
CEC09F2D2A6DC60500980857 /* UUPDetails.swift in Sources */,
|
||||
CEC09F352A6DF33C00980857 /* BuildsListView.swift in Sources */,
|
||||
84EB35722A870EA7004F252E /* ShowWindowButtonView.swift in Sources */,
|
||||
CEC09F392A6E5D5C00980857 /* BuildEditions.swift in Sources */,
|
||||
844A8F032A86E86F009A389C /* EULAView.swift in Sources */,
|
||||
CEC09F2F2A6DC72000980857 /* UUPEditions.swift in Sources */,
|
||||
CEC09F2B2A6DC40900980857 /* UUPBuilds.swift in Sources */,
|
||||
);
|
||||
|
|
@ -1948,12 +2004,36 @@
|
|||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
CEC8BC522A7B55D80042878F /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CEC8BC512A7B55D80042878F /* en */,
|
||||
CEC8BC532A7B56280042878F /* ja */,
|
||||
FFB1B5272A929A2800B95D56 /* zh-Hant */,
|
||||
F65FBD842AA77422007637B0 /* zh-Hans */,
|
||||
F65FBD852AA77426007637B0 /* zh-HK */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FFB1B5282A929CEC00B95D56 /* CrystalFetch-InfoPlist.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
FFB1B5292A929CEC00B95D56 /* zh-Hant */,
|
||||
);
|
||||
name = "CrystalFetch-InfoPlist.strings";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
CEC09F182A6BB66300980857 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = CEC0A3082A71BBA900980857 /* Build.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
|
|
@ -2018,6 +2098,7 @@
|
|||
baseConfigurationReference = CEC0A3082A71BBA900980857 /* Build.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
|
|
@ -2197,6 +2278,7 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_PREFIX:default=llc.turing).CrystalFetch.mkisofs";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
WARNING_CFLAGS = "-Wno-error=incompatible-function-pointer-types";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
|
@ -2240,6 +2322,7 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_PREFIX:default=llc.turing).CrystalFetch.mkisofs";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
WARNING_CFLAGS = "-Wno-error=incompatible-function-pointer-types";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
|
|
|||
313
Extras/esd2iso.sh
Executable file
313
Extras/esd2iso.sh
Executable file
|
|
@ -0,0 +1,313 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# w11arm_esd2iso - download and convert Microsoft ESD files for Windows 11 ARM to ISO
|
||||
#
|
||||
# Copyright (C) 2023 Paul Rockwell
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA#
|
||||
#
|
||||
# Credit: Location and methods of obtaining Microsoft ESD distributions and
|
||||
# Microsoft Product catalog from b0gdanw "ESD to ISO on macOS.txt" https://gist.github.com/b0gdanw/e36ea84828dbd19e03eff6158f1fc77c
|
||||
#
|
||||
|
||||
versionID="4.0.3 (17-September-2023)"
|
||||
version="w11arm_esd2iso ${versionID}\n"
|
||||
verbosityLevel=0
|
||||
|
||||
awk="/usr/bin/awk"
|
||||
genisoimage="$(command -v mkisofs)"
|
||||
|
||||
declare -a lTags
|
||||
declare -a lDesc
|
||||
|
||||
usage() {
|
||||
echo "Usage:\n"
|
||||
echo "$0 [-v]"
|
||||
echo "$0 [-Vh]"
|
||||
echo "\nOptions:"
|
||||
echo "\t-h\tPrint usage and exit"
|
||||
echo "\t-v\tEnable verbose output"
|
||||
echo "\t-V\tPrint program version and exit"
|
||||
}
|
||||
printVersion() {
|
||||
echo $version
|
||||
}
|
||||
verboseOn() {
|
||||
if (( verbosityLevel == 0 )); then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
extractEsd(){
|
||||
|
||||
local eFile
|
||||
local eDir
|
||||
local retVal
|
||||
local esdImageCount
|
||||
local bootWimFile
|
||||
local installWimFile
|
||||
local images
|
||||
local imageIndex
|
||||
local imageEdition
|
||||
local beQuiet
|
||||
|
||||
eFile=$1
|
||||
eDir=$2
|
||||
beQuiet="--quiet"
|
||||
bootWimFile=$eDir/sources/boot.wim
|
||||
installWimFile=$eDir/sources/install.wim
|
||||
images=("4" "5")
|
||||
|
||||
verboseOn && beQuiet=""
|
||||
|
||||
esdImageCount=$(wimlib-imagex info $eFile | $awk '/Image Count:/ {print $3}')
|
||||
verboseOn && echo "[DEBUG] image count in ESD: $esdImageCount"
|
||||
for (( i = 6; i <= esdImageCount; i++ )); do
|
||||
images+=("$i")
|
||||
done
|
||||
|
||||
#---------------
|
||||
# Extract image 1 in the ESD to create the boot environment
|
||||
#---------------
|
||||
|
||||
echo "\nApplying boot files to the image"
|
||||
wimlib-imagex apply $eFile 1 $eDir $beQuiet 2>/dev/null
|
||||
retVal=$?
|
||||
if (( retVal != 0 )); then
|
||||
echo "[ERROR] Extract of boot files failed"
|
||||
return $retVal
|
||||
fi
|
||||
|
||||
echo "Boot files successfully applied to image"
|
||||
|
||||
#---------------
|
||||
# Create the boot.wim file that contains WinPE and Windows Setup
|
||||
# Images 2 and 3 in the ESD contain these components
|
||||
#
|
||||
# Important: image 3 in the ESD must be marked as bootable when
|
||||
# transferred to boot.wim or else the installer will fail
|
||||
#---------------
|
||||
|
||||
echo "\nAdding WinPE and Windows Setup to the image"
|
||||
wimlib-imagex export $eFile 2 $bootWimFile --compress=LZX --chunk-size 32K $beQuiet
|
||||
retVal=$?
|
||||
if (( retVal != 0 )); then
|
||||
echo "[ERROR] Add of WinPE failed"
|
||||
return $retVal
|
||||
fi
|
||||
|
||||
wimlib-imagex export $eFile 3 $bootWimFile --compress=LZX --chunk-size 32K --boot $beQuiet
|
||||
retVal=$?
|
||||
if (( retVal != 0 )); then
|
||||
echo "[ERROR] Add of Windows Setup failed"
|
||||
return $retVal
|
||||
fi
|
||||
echo "WinPE and Windows Setup added successfully to image\n"
|
||||
|
||||
verboseOn && {
|
||||
echo "[DEBUG] contents of $bootWimFile"
|
||||
wimlib-imagex info $bootWimFile
|
||||
}
|
||||
|
||||
|
||||
#---------------
|
||||
# Create the install.wim file that contains the files that Setup will install
|
||||
# Images 4, 5, (and 6 if it exists) in the ESD contain these components
|
||||
#---------------
|
||||
|
||||
for imageIndex in ${images[*]}; do
|
||||
imageEdition="$(wimlib-imagex info $eFile $imageIndex | grep '^Description:' | sed 's/Description:[ \t]*//')"
|
||||
echo "\nAdding $imageEdition to the image"
|
||||
wimlib-imagex export $eFile $imageIndex $installWimFile --compress=LZMS --chunk-size 128K $beQuiet
|
||||
retVal=$?
|
||||
if (( retVal != 0 )); then
|
||||
echo "[ERROR] Addition of $imageIndex to the image failed"
|
||||
return $retVal
|
||||
fi
|
||||
echo "$imageEdition added successfully to the image"
|
||||
|
||||
done
|
||||
|
||||
echo "\nAll Windows editions added to image"
|
||||
|
||||
verboseOn && {
|
||||
echo "[DEBUG] contents of $installWimFile"
|
||||
wimlib-imagex info $installWimFile
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
buildIso(){
|
||||
local iDir=$1
|
||||
local iFile=$2
|
||||
local iLabel=$3
|
||||
local elToritoBootFile
|
||||
|
||||
iDir=$1
|
||||
iFile=$2
|
||||
|
||||
if [ -e $iFile ]; then
|
||||
echo "\t[INFO] File $iFile exists, removing it"
|
||||
rm -rf $iFile
|
||||
fi
|
||||
|
||||
elToritoBootFile=$iDir/efi/microsoft/boot/efisys.bin
|
||||
|
||||
#
|
||||
# Create the ISO file
|
||||
#
|
||||
|
||||
#$hdiutil makehybrid -o $iFile -iso -udf -hard-disk-boot -eltorito-boot $elToritoBootFile $iDir
|
||||
"$genisoimage" -b "efi/microsoft/boot/efisys.bin" --no-emul-boot \
|
||||
--udf -iso-level 3 --hide "*" -V "$iLabel" -o "$iFile" $iDir
|
||||
return $?
|
||||
}
|
||||
|
||||
#-------------------
|
||||
#
|
||||
# Start of program
|
||||
#
|
||||
#-------------------
|
||||
|
||||
#-------------------
|
||||
# Bug fix - 16-Sept-2023 PER
|
||||
#
|
||||
# aria2c seems to have an issue with Sonoma: https://github.com/aria2/aria2/issues/2083
|
||||
# This results in an error 134 that was displayed when trying to download the
|
||||
# Windows 11 ARM catalog from Microsoft.
|
||||
#
|
||||
# suggested workaround is to set LC_MESSAGES environment variable
|
||||
#-------------------
|
||||
|
||||
export LC_MESSAGES="C"
|
||||
|
||||
#-------------------
|
||||
#
|
||||
# Process arguments
|
||||
#
|
||||
#-------------------
|
||||
|
||||
while getopts ":hr:vV" opt; do
|
||||
case $opt in
|
||||
h)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
v)
|
||||
let verbosityLevel+=1
|
||||
;;
|
||||
V)
|
||||
printVersion
|
||||
exit 1
|
||||
;;
|
||||
:)
|
||||
echo "[ERROR] Option -$OPTARG requires an argument"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
\?)
|
||||
echo "[ERROR] Invalid option: -$OPTARG\n"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift "$((OPTIND-1))"
|
||||
|
||||
|
||||
printVersion
|
||||
|
||||
esdFile="$1"
|
||||
isoFile="$2"
|
||||
isoLabel="$3"
|
||||
|
||||
#-------------------
|
||||
# Check number of arguments
|
||||
# One argument is allowed when using the -r option for restart
|
||||
# No arguments are allowed otherwise
|
||||
#-------------------
|
||||
|
||||
if (( $# > 3 )); then
|
||||
echo "[ERROR] Too many arguments"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
workingDir="$(mktemp -q -d ./esd2iso_temp.XXXXXX)"
|
||||
if (( $? != 0 )); then
|
||||
echo "[ERROR] Unable to create work directory, exiting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#---------------
|
||||
#
|
||||
# extDir is the "extract directory" where we're going to extract the ESD
|
||||
# and evenutally build the ISO from. It's a subdirectory of the working/temp directory
|
||||
#
|
||||
##---------------
|
||||
|
||||
extDir=$workingDir/ESD_ISO
|
||||
mkdir $extDir
|
||||
|
||||
echo "\nStep 3: Building installation image from ESD distribution"
|
||||
extractEsd $esdFile $extDir
|
||||
retVal=$?
|
||||
if (( retVal != 0 )); then
|
||||
echo "[ERROR] Installation image build failed with error code $retVal"
|
||||
echo "Work directory $workingDir was not deleted, use for debugging"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#---------------
|
||||
# At this point we no longer need the ESD file as it's already extracted
|
||||
# In order to reduce disk space reauirements, delete the ESD file unless we
|
||||
# have set the environment variable keepDownloads
|
||||
#---------------
|
||||
|
||||
if [[ "x${keepDownloads}" == "x" ]]; then
|
||||
echo \n"ESD added successfully to installation image and is no longer needed.\nDeleting it to save disk space."
|
||||
verboseOn && echo "Deleting ESD file ${esdFile}"
|
||||
rm -rf ${esdFile}
|
||||
retVal=$?
|
||||
if (( retVal != 0 )); then
|
||||
echo "[WARNING] Deletion of ESD file encountered a problem."
|
||||
echo " The ISO build can continue, but will consume an addtional 5 GB of disk space."
|
||||
else
|
||||
echo "ESD file deleted successfully\n"
|
||||
fi
|
||||
else
|
||||
verboseOn && echo "[DEBUG] keepDownloads is set - keeping ESD download"
|
||||
fi
|
||||
|
||||
echo "\nStep 3 complete - installation image built"
|
||||
|
||||
echo "\nStep 4: Creating ISO $isoFile from the installation image\n"
|
||||
|
||||
buildIso $extDir $isoFile $isoLabel
|
||||
retVal=$?
|
||||
if (( retVal != 0 )); then
|
||||
echo "[ERROR] ISO was NOT created"
|
||||
echo "Working directory $workingDir was not deleted, use for debugging"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Step 4 complete - ISO created"
|
||||
echo "\nCleaning up work directory"
|
||||
rm -rf $workingDir
|
||||
echo "Done!"
|
||||
exit 0
|
||||
6
PRIVACY.md
Normal file
6
PRIVACY.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Privacy Policy
|
||||
==============
|
||||
CrystalFetch will make network requests to uupdump.net and microsoft.com.
|
||||
|
||||
* UUPDump collects IP addresses in order to enforce rate limiting of API requests.
|
||||
* Microsoft collects IP addresses for a variety of purposes as detailed in their [privacy statement](https://privacy.microsoft.com/en-us/privacystatement).
|
||||
|
|
@ -22,6 +22,11 @@ Credits
|
|||
-------
|
||||
CrystalFetch uses [UUPDump][3] APIs and converter scripts.
|
||||
|
||||
CrystalFetch uses [esd2iso][4] by Technogeezer.
|
||||
|
||||
This project is not affiliated with Microsoft Corporation. Windows® is a registered trademark of Microsoft Corporation.
|
||||
|
||||
[1]: https://github.com/TuringSoftware/CrystalFetch/actions?query=event%3Arelease+workflow%3ABuild
|
||||
[2]: https://mac.getutm.app
|
||||
[3]: https://uupdump.net
|
||||
[4]: https://communities.vmware.com/t5/VMware-Fusion-Documents/w11arm-esd2iso-a-utility-to-create-Windows-11-ARM-ISOs-from/ta-p/2957381
|
||||
|
|
|
|||
32
README.zh-HK.md
Normal file
32
README.zh-HK.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
CrystalFetch
|
||||
============
|
||||
[][1]
|
||||
|
||||
CrystalFetch 是一個 macOS 應用程式,用於製作 Windows® 11 安裝程式 ISO 映像檔。它可以與 [UTM 虛擬電腦][3]及其他虛擬電腦解決方案一齊使用。
|
||||
|
||||
注意:CrystalFetch 與 Microsoft 無關聯,安裝 Windows® 11 需要有效的許可證(產品金鑰)。
|
||||
|
||||
<p align="center">
|
||||
<img alt="CrystalFetch logo" src="Source/Assets.xcassets/AppIcon.appiconset/icon_128x128.png" srcset="Source/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png 2x" /><br />
|
||||
<img alt="CrystalFetch screenshot" src="Extras/screen.png" />
|
||||
</p>
|
||||
|
||||
構建
|
||||
--------
|
||||
1. 確保你使用 `git submodule update --init` 來獲取子模組。
|
||||
2. 如你有 Apple Developer 付費賬戶,請複製 `CodeSigning.xcconfig.sample` 到 `CodeSigning.xcconfig`,並在此檔案中填寫你的開發人員訊息。
|
||||
3. 如你沒有 Apple Developer 付費賬戶,你需要禁用庫驗證。對於項目中的每一個構建目標,轉至“Signing & Capabilities”,之後選取“Disable Library Validation”核取方塊。
|
||||
4. 現在你就可以由 Xcode 構建並執行此項目了。
|
||||
|
||||
致謝
|
||||
-------
|
||||
CrystalFetch 使用了 [UUPDump][3] 的 API 與轉換程式碼。
|
||||
|
||||
CrystalFetch 使用了由 Technogeezer 製作的 [esd2iso][4]。
|
||||
|
||||
此項目與 Microsoft Corporation 無關聯。Windows® 為 Microsoft Corporation 的註冊商標。
|
||||
|
||||
[1]: https://github.com/TuringSoftware/CrystalFetch/actions?query=event%3Arelease+workflow%3ABuild
|
||||
[2]: https://mac.getutm.app
|
||||
[3]: https://uupdump.net
|
||||
[4]: https://communities.vmware.com/t5/VMware-Fusion-Documents/w11arm-esd2iso-a-utility-to-create-Windows-11-ARM-ISOs-from/ta-p/2957381
|
||||
32
README.zh-Hans.md
Normal file
32
README.zh-Hans.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
CrystalFetch
|
||||
============
|
||||
[][1]
|
||||
|
||||
CrystalFetch 是一个用于创建 Windows® 11 安装 ISO 映像的 macOS 应用程序。它可以与 [UTM 虚拟机][3]及其他虚拟机解决方案一起使用。
|
||||
|
||||
注意:CrystalFetch 不隶属于 Microsoft(微软)。安装 Windows® 11 需要有效的许可证(产品密钥)。
|
||||
|
||||
<p align="center">
|
||||
<img alt="CrystalFetch logo" src="Source/Assets.xcassets/AppIcon.appiconset/icon_128x128.png" srcset="Source/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png 2x" /><br />
|
||||
<img alt="CrystalFetch screenshot" src="Extras/screen.png" />
|
||||
</p>
|
||||
|
||||
编译
|
||||
--------
|
||||
1. 确保使用 `git submodule update --init` 来获取子模块。
|
||||
2. 若你有付费的 Apple Developer 账号,请将 `CodeSigning.xcconfig.sample` 拷贝到 `CodeSigning.xcconfig`,并用你的开发者信息来填写该文件。
|
||||
3. 若你没有付费的 Apple Developer 账号,需要禁用库验证(library validation)。对于此项目中的每个编译目标(build target),请前往“Signing & Capabilities”并勾选“Disable Library Validation”。
|
||||
4. 现在可以从 Xcode 编译和运行此项目了。
|
||||
|
||||
致谢
|
||||
-------
|
||||
CrystalFetch 使用了 [UUPDump][3] 的 API 与转换脚本。
|
||||
|
||||
CrystalFetch 使用了由 Technogeezer 编写的 [esd2iso][4]。
|
||||
|
||||
此项目不隶属于 Microsoft Corporation。Windows® 是 Microsoft Corporation 的注册商标。
|
||||
|
||||
[1]: https://github.com/TuringSoftware/CrystalFetch/actions?query=event%3Arelease+workflow%3ABuild
|
||||
[2]: https://mac.getutm.app
|
||||
[3]: https://uupdump.net
|
||||
[4]: https://communities.vmware.com/t5/VMware-Fusion-Documents/w11arm-esd2iso-a-utility-to-create-Windows-11-ARM-ISOs-from/ta-p/2957381
|
||||
|
|
@ -52,7 +52,7 @@ struct BuildConfigView: View {
|
|||
}.padding(.bottom, 5)
|
||||
Section("Language") {
|
||||
Picker("", selection: $selectedLocale) {
|
||||
ForEach(details.languages) { language in
|
||||
ForEach(details.sortedLanguages) { language in
|
||||
Text(language.display).tag(language.code)
|
||||
}
|
||||
}.onChange(of: selectedLocale) { newValue in
|
||||
|
|
@ -78,6 +78,9 @@ struct BuildConfigView: View {
|
|||
}))
|
||||
}
|
||||
}
|
||||
if build.arch == "arm64" && Float(build.build) ?? 0 < 21390.0 {
|
||||
Text("Note: This build does not work for virtualization on Apple Silicon.")
|
||||
}
|
||||
}.disabled(worker.isBusy)
|
||||
}
|
||||
Spacer()
|
||||
|
|
@ -136,7 +139,7 @@ struct BuildConfigView: View {
|
|||
if let lastSelectedLocale = lastSelectedLocale {
|
||||
selectedLocale = lastSelectedLocale
|
||||
} else {
|
||||
selectedLocale = worker.defaultLocale
|
||||
selectedLocale = Worker.defaultLocale ?? "netural"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ struct BuildDetails {
|
|||
let arch: String
|
||||
let build: String
|
||||
let created: Date
|
||||
|
||||
var sortedLanguages: [Language] {
|
||||
return languages.sorted(using: KeyPathComparator(\.display))
|
||||
}
|
||||
|
||||
static var empty = BuildDetails()
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,12 @@ struct ContentView: View {
|
|||
ProgressView()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigation) {
|
||||
ShowWindowButtonView(id: "ESDConvert") {
|
||||
Label("Simple…", systemImage: "arrowshape.turn.up.backward.fill")
|
||||
}.disabled(worker.isBusy)
|
||||
.help("Build installation for the latest release through ESD conversion.")
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Button {
|
||||
worker.refresh(findDefault: true)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,28 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>crystalfetch</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>dl.delivery.mp.microsoft.com</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>tlu.dl.delivery.mp.microsoft.com</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ actor Downloader {
|
|||
|
||||
private func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
totalDownloadedSize += bytesWritten
|
||||
progressCallback?(totalDownloadedSize, totalExpectedSize)
|
||||
let expectedSize = totalExpectedSize > 0 ? totalExpectedSize : totalBytesExpectedToWrite
|
||||
progressCallback?(totalDownloadedSize, expectedSize)
|
||||
}
|
||||
|
||||
private func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
|
|
@ -99,7 +100,7 @@ actor Downloader {
|
|||
/// - downloadUrl: What to download
|
||||
/// - destinationUrl: Where to put it
|
||||
/// - size: Estimated size for progress updates
|
||||
func enqueue(downloadUrl: URL, to destinationUrl: URL, size: Int64) {
|
||||
func enqueue(downloadUrl: URL, to destinationUrl: URL, size: Int64 = 0) {
|
||||
let task = session.downloadTask(with: downloadUrl)
|
||||
queue.append((task: task, destinationUrl: destinationUrl, retry: kMaxRetries))
|
||||
totalExpectedSize += size
|
||||
|
|
@ -107,6 +108,9 @@ actor Downloader {
|
|||
|
||||
/// Start downloading a single item from the queue and retry if the download is interrupted
|
||||
private func dequeue() async throws {
|
||||
guard !queue.isEmpty else {
|
||||
return
|
||||
}
|
||||
let (task, destinationUrl, retry) = queue.removeFirst()
|
||||
let debugIdentifier = task.originalRequest?.url?.absoluteString ?? "(unknown request)"
|
||||
NSLog("Downloading %@ to %@ (retries left: %d)", debugIdentifier, destinationUrl.path, retry)
|
||||
|
|
@ -122,7 +126,7 @@ actor Downloader {
|
|||
try FileManager.default.moveItem(at: resultUrl, to: destinationUrl)
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
NSLog("Downloading %@ failed: ", debugIdentifier, error.localizedDescription)
|
||||
NSLog("Downloading %@ failed: %@", debugIdentifier, error.localizedDescription)
|
||||
if retry > 0 {
|
||||
let newTask: URLSessionDownloadTask
|
||||
if let resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
|
||||
|
|
|
|||
364
Source/ESDCatalog/ESDCatalog.swift
Normal file
364
Source/ESDCatalog/ESDCatalog.swift
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
//
|
||||
// Copyright © 2023 Turing Software, LLC. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
actor ESDCatalog {
|
||||
struct File: Equatable {
|
||||
let name: String
|
||||
let languageCode: String
|
||||
let languagePretty: String
|
||||
let edition: String
|
||||
let editionPretty: String
|
||||
let architecture: String
|
||||
let architecturePretty: String
|
||||
let size: Int64
|
||||
let sha1: String
|
||||
let filePath: String
|
||||
let isRetailOnly: Bool
|
||||
let eula: String?
|
||||
}
|
||||
|
||||
private(set) var files: [File]
|
||||
|
||||
init(from data: Data) async throws {
|
||||
let result: ESDCatalogParser.Result = try await withCheckedThrowingContinuation { continuation in
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let parser = XMLParser(data: data)
|
||||
let coordinator = ESDCatalogParser()
|
||||
coordinator.continuation = continuation
|
||||
parser.delegate = coordinator
|
||||
if !parser.parse() && coordinator.continuation != nil {
|
||||
continuation.resume(throwing: parser.parserError ?? ESDCatalogError.unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
files = try result.files.map { parseFile in
|
||||
let languageCode = parseFile.LanguageCode ?? "default"
|
||||
guard let localization = result.localization[languageCode] else {
|
||||
throw ESDCatalogError.localizationNotFound(languageCode)
|
||||
}
|
||||
return try Self.parseResult(parseFile, localization: localization, eulas: result.eulas)
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseResult(_ result: ESDCatalogParser.File, localization: [String: String], eulas: [String: String?]) throws -> File {
|
||||
guard let name = result.FileName else {
|
||||
throw ESDCatalogError.missingElement("FileName", "Unknown")
|
||||
}
|
||||
guard let languageCode = result.LanguageCode else {
|
||||
throw ESDCatalogError.missingElement("LanguageCode", name)
|
||||
}
|
||||
guard let languagePretty = result.Language else {
|
||||
throw ESDCatalogError.missingElement("Language", name)
|
||||
}
|
||||
guard let edition = result.Edition else {
|
||||
throw ESDCatalogError.missingElement("Edition", name)
|
||||
}
|
||||
guard let editionLoc = result.Edition_Loc else {
|
||||
throw ESDCatalogError.missingElement("Edition_Loc", name)
|
||||
}
|
||||
let editionPretty = localize(editionLoc, localization: localization)
|
||||
guard let architecture = result.Architecture else {
|
||||
throw ESDCatalogError.missingElement("Architecture", name)
|
||||
}
|
||||
guard let architectureLoc = result.Architecture_Loc else {
|
||||
throw ESDCatalogError.missingElement("Architecture_Loc", name)
|
||||
}
|
||||
let architecturePretty = localize(architectureLoc, localization: localization)
|
||||
guard let size = result.Size else {
|
||||
throw ESDCatalogError.missingElement("Size", name)
|
||||
}
|
||||
guard let sha1 = result.Sha1 else {
|
||||
throw ESDCatalogError.missingElement("Sha1", name)
|
||||
}
|
||||
guard let filePath = result.FilePath else {
|
||||
throw ESDCatalogError.missingElement("FilePath", name)
|
||||
}
|
||||
guard let isRetailOnly = result.IsRetailOnly else {
|
||||
throw ESDCatalogError.missingElement("IsRetailOnly", name)
|
||||
}
|
||||
let eula = eulas[languageCode] ?? nil
|
||||
return File(name: name,
|
||||
languageCode: languageCode,
|
||||
languagePretty: languagePretty,
|
||||
edition: edition,
|
||||
editionPretty: editionPretty,
|
||||
architecture: architecture,
|
||||
architecturePretty: architecturePretty,
|
||||
size: size,
|
||||
sha1: sha1,
|
||||
filePath: filePath,
|
||||
isRetailOnly: isRetailOnly,
|
||||
eula: eula)
|
||||
}
|
||||
|
||||
private static func localize(_ string: String, localization: [String: String]) -> String {
|
||||
let lookup = String(string.dropFirst().dropLast())
|
||||
return localization[lookup] ?? lookup
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class ESDCatalogParser: NSObject, XMLParserDelegate {
|
||||
enum Inside: String {
|
||||
case MCT
|
||||
case Catalogs
|
||||
case Catalog
|
||||
case PublishedMedia
|
||||
case Files
|
||||
case File
|
||||
case Languages
|
||||
case Language
|
||||
case EULAs
|
||||
case EULA
|
||||
}
|
||||
|
||||
struct File {
|
||||
var FileName: String?
|
||||
var LanguageCode: String?
|
||||
var Language: String?
|
||||
var Edition: String?
|
||||
var Architecture: String?
|
||||
var Size: Int64?
|
||||
var Sha1: String?
|
||||
var FilePath: String?
|
||||
var Architecture_Loc: String?
|
||||
var Edition_Loc: String?
|
||||
var IsRetailOnly: Bool?
|
||||
}
|
||||
|
||||
struct Language {
|
||||
var LanguageCode: String
|
||||
var Localization: [String: String] = [:]
|
||||
}
|
||||
|
||||
struct EULA {
|
||||
var LanguageCode: String?
|
||||
var URL: String?
|
||||
}
|
||||
|
||||
typealias Result = (files: [File], localization: [String: [String: String]], eulas: [String: String?])
|
||||
|
||||
var continuation: CheckedContinuation<Result, Error>?
|
||||
private var inside: Inside?
|
||||
private var field: String?
|
||||
private var currentRecord: Any?
|
||||
private var files: [File] = []
|
||||
private var localization: [String: [String: String]] = [:]
|
||||
private var eulas: [String: String?] = [:]
|
||||
|
||||
func parserDidStartDocument(_ xmlParser: XMLParser) {
|
||||
inside = nil
|
||||
}
|
||||
|
||||
func parserDidEndDocument(_ xmlParser: XMLParser) {
|
||||
if let c = continuation {
|
||||
continuation = nil
|
||||
c.resume(returning: (files, localization, eulas))
|
||||
}
|
||||
}
|
||||
|
||||
func parser(_ xmlParser: XMLParser, parseErrorOccurred parseError: Error) {
|
||||
if let c = continuation {
|
||||
continuation = nil
|
||||
c.resume(throwing: parseError)
|
||||
}
|
||||
xmlParser.abortParsing()
|
||||
}
|
||||
|
||||
func parser(_ xmlParser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
|
||||
switch inside {
|
||||
case .MCT:
|
||||
guard expect(elementName, named: .Catalogs, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
inside = .Catalogs
|
||||
case .Catalogs:
|
||||
guard expect(elementName, named: .Catalog, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
inside = .Catalog
|
||||
case .Catalog:
|
||||
guard expect(elementName, named: .PublishedMedia, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
inside = .PublishedMedia
|
||||
case .PublishedMedia:
|
||||
if elementName == Inside.Files.rawValue {
|
||||
inside = .Files
|
||||
} else if elementName == Inside.Languages.rawValue {
|
||||
inside = .Languages
|
||||
} else if elementName == Inside.EULAs.rawValue {
|
||||
inside = .EULAs
|
||||
} else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
||||
}
|
||||
case .Files:
|
||||
guard expect(elementName, named: .File, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
inside = .File
|
||||
currentRecord = File()
|
||||
case .Languages:
|
||||
guard expect(elementName, named: .Language, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
guard let languageCode = attributeDict["LanguageCode"] else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.attributeNotFound("LanguageCode"))
|
||||
return
|
||||
}
|
||||
inside = .Language
|
||||
currentRecord = Language(LanguageCode: languageCode)
|
||||
case .EULAs:
|
||||
guard expect(elementName, named: .EULA, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
inside = .EULA
|
||||
currentRecord = EULA()
|
||||
case nil:
|
||||
guard expect(elementName, named: .MCT, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
inside = .MCT
|
||||
case .File, .Language, .EULA:
|
||||
guard field == nil else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
||||
return
|
||||
}
|
||||
field = elementName
|
||||
}
|
||||
}
|
||||
|
||||
func parser(_ xmlParser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
|
||||
guard field == nil else {
|
||||
if field == elementName {
|
||||
field = nil
|
||||
} else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
||||
}
|
||||
return
|
||||
}
|
||||
guard elementName == inside?.rawValue else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
||||
return
|
||||
}
|
||||
switch inside! {
|
||||
case .MCT:
|
||||
inside = nil
|
||||
case .Catalogs:
|
||||
inside = .MCT
|
||||
case .Catalog:
|
||||
inside = .Catalogs
|
||||
case .PublishedMedia:
|
||||
inside = .Catalog
|
||||
case .File:
|
||||
inside = .Files
|
||||
files.append(currentRecord as! File)
|
||||
case .Language:
|
||||
inside = .Languages
|
||||
let record = currentRecord as! Language
|
||||
localization[record.LanguageCode] = record.Localization
|
||||
case .EULA:
|
||||
inside = .EULAs
|
||||
let record = currentRecord as! EULA
|
||||
if let languageCode = record.LanguageCode {
|
||||
eulas[languageCode] = record.URL
|
||||
}
|
||||
case .Files, .Languages, .EULAs:
|
||||
inside = .PublishedMedia
|
||||
}
|
||||
}
|
||||
|
||||
func parser(_ xmlParser: XMLParser, foundCharacters string: String) {
|
||||
guard let field = field else {
|
||||
return
|
||||
}
|
||||
if inside == .File {
|
||||
parseFileElement(string, for: field)
|
||||
} else if inside == .Language {
|
||||
parseLanguageElement(string, for: field)
|
||||
} else if inside == .EULA {
|
||||
parseEULAElement(string, for: field)
|
||||
} else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.unexpectedString(string, field))
|
||||
}
|
||||
}
|
||||
|
||||
private func parseFileElement(_ element: String, for field: String) {
|
||||
var record = currentRecord as! File
|
||||
switch field {
|
||||
case "FileName": record.FileName = element
|
||||
case "LanguageCode": record.LanguageCode = element
|
||||
case "Language": record.Language = element
|
||||
case "Edition": record.Edition = element
|
||||
case "Architecture": record.Architecture = element
|
||||
case "Size": record.Size = Int64(element)
|
||||
case "Sha1": record.Sha1 = element
|
||||
case "FilePath": record.FilePath = element
|
||||
case "Architecture_Loc": record.Architecture_Loc = element
|
||||
case "Edition_Loc": record.Edition_Loc = element
|
||||
case "IsRetailOnly": record.IsRetailOnly = element == "True"
|
||||
default: break
|
||||
}
|
||||
currentRecord = record
|
||||
}
|
||||
|
||||
private func parseLanguageElement(_ element: String, for field: String) {
|
||||
var record = currentRecord as! Language
|
||||
record.Localization[field] = element
|
||||
currentRecord = record
|
||||
}
|
||||
|
||||
private func parseEULAElement(_ element: String, for field: String) {
|
||||
var record = currentRecord as! EULA
|
||||
switch field {
|
||||
case "LanguageCode": record.LanguageCode = element
|
||||
case "URL": record.URL = element
|
||||
default: break
|
||||
}
|
||||
currentRecord = record
|
||||
}
|
||||
|
||||
private func expect(_ elementName: String, named element: Inside, in xmlParser: XMLParser) -> Bool {
|
||||
guard elementName == element.rawValue else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
enum ESDCatalogError: Error {
|
||||
case unknown
|
||||
case invalidElement(String)
|
||||
case attributeNotFound(String)
|
||||
case unexpectedString(String, String)
|
||||
case localizationNotFound(String)
|
||||
case missingElement(String, String)
|
||||
}
|
||||
|
||||
extension ESDCatalogError: LocalizedError {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .unknown: return NSLocalizedString("Unknown parser error.", comment: "ESDCatalog")
|
||||
case .invalidElement(let element): return String.localizedStringWithFormat(NSLocalizedString("Failed to parse element '%@'", comment: "ESDCatalog"), element)
|
||||
case .attributeNotFound(let string): return String.localizedStringWithFormat(NSLocalizedString("Attribute '%@' not found", comment: "ESDCatalog"), string)
|
||||
case .unexpectedString(let string, let field): return String.localizedStringWithFormat(NSLocalizedString("Unexpected string '%@' for field '%@'", comment: "ESDCatalog"), string, field)
|
||||
case .localizationNotFound(let string): return String.localizedStringWithFormat(NSLocalizedString("Localization '%@' not found", comment: "ESDCatalog"), string)
|
||||
case .missingElement(let element, let fileName): return String.localizedStringWithFormat(NSLocalizedString("Missing element '%@' in '%@'", comment: "ESDCatalog"), element, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
246
Source/ESDCatalog/MCTCatalogs.swift
Normal file
246
Source/ESDCatalog/MCTCatalogs.swift
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
//
|
||||
// Copyright © 2025 Turing Software, LLC. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
actor MCTCatalogs {
|
||||
struct Version: Equatable {
|
||||
let latestCabUrl: URL?
|
||||
let releases: [Release]
|
||||
}
|
||||
|
||||
struct Release: Equatable, Hashable, Identifiable {
|
||||
let build: String
|
||||
let date: Date?
|
||||
let cabUrl: URL
|
||||
|
||||
var id: Int {
|
||||
hashValue
|
||||
}
|
||||
}
|
||||
|
||||
enum Windows: Int {
|
||||
case windows10 = 10
|
||||
case windows11 = 11
|
||||
}
|
||||
|
||||
private(set) var versions: [Windows: Version] = [:]
|
||||
|
||||
init(from data: Data) async throws {
|
||||
let result = try await withCheckedThrowingContinuation { continuation in
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let parser = XMLParser(data: data)
|
||||
let coordinator = MCTCatalogsParser()
|
||||
coordinator.continuation = continuation
|
||||
parser.delegate = coordinator
|
||||
if !parser.parse() && coordinator.continuation != nil {
|
||||
continuation.resume(throwing: parser.parserError ?? ESDCatalogError.unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
let results = try result.map { try Self.parseVersion($0) }
|
||||
for (number, version) in results {
|
||||
if let windows = Windows(rawValue: number) {
|
||||
versions[windows] = version
|
||||
} else {
|
||||
NSLog("Ignoring unknown Windows version: %@", number)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseVersion(_ result: MCTCatalogsParser.Version) throws -> (number: Int, version: Version) {
|
||||
let latestCabUrl: URL?
|
||||
if let latestCabLink = result.latestCabLink {
|
||||
latestCabUrl = URL(string: latestCabLink)!
|
||||
} else {
|
||||
latestCabUrl = nil
|
||||
}
|
||||
guard let number = Int(result.number) else {
|
||||
throw ESDCatalogError.missingElement("number", "version")
|
||||
}
|
||||
let releases = try result.releases.map { try Self.parseRelease($0) }
|
||||
return (number, Version(latestCabUrl: latestCabUrl, releases: releases))
|
||||
}
|
||||
|
||||
private static func parseRelease(_ result: MCTCatalogsParser.Release) throws -> Release {
|
||||
let date: Date?
|
||||
if let dateString = result.date {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyyMMdd"
|
||||
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
date = dateFormatter.date(from: dateString)
|
||||
} else {
|
||||
date = nil
|
||||
}
|
||||
guard let cabLink = result.cabLink else {
|
||||
throw ESDCatalogError.missingElement("cabLink", "release")
|
||||
}
|
||||
let cabUrl: URL = URL(string: cabLink)!
|
||||
return Release(build: result.build, date: date, cabUrl: cabUrl)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class MCTCatalogsParser: NSObject, XMLParserDelegate {
|
||||
enum Inside: String {
|
||||
case productsDb
|
||||
case versions
|
||||
case version
|
||||
case latestCabLink
|
||||
case releases
|
||||
case release
|
||||
case date
|
||||
case cabLink
|
||||
}
|
||||
|
||||
struct Version {
|
||||
var number: String
|
||||
var latestCabLink: String?
|
||||
var releases: [Release] = []
|
||||
}
|
||||
|
||||
struct Release {
|
||||
var build: String
|
||||
var date: String?
|
||||
var cabLink: String?
|
||||
}
|
||||
|
||||
var continuation: CheckedContinuation<[Version], Error>?
|
||||
private var inside: Inside?
|
||||
private var versions: [Version] = []
|
||||
private var currentVersion: Version?
|
||||
private var currentRelease: Release?
|
||||
|
||||
func parserDidStartDocument(_ xmlParser: XMLParser) {
|
||||
inside = nil
|
||||
}
|
||||
|
||||
func parserDidEndDocument(_ xmlParser: XMLParser) {
|
||||
if let c = continuation {
|
||||
continuation = nil
|
||||
c.resume(returning: versions)
|
||||
}
|
||||
}
|
||||
|
||||
func parser(_ xmlParser: XMLParser, parseErrorOccurred parseError: Error) {
|
||||
if let c = continuation {
|
||||
continuation = nil
|
||||
c.resume(throwing: parseError)
|
||||
}
|
||||
xmlParser.abortParsing()
|
||||
}
|
||||
|
||||
func parser(_ xmlParser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
|
||||
switch inside {
|
||||
case nil:
|
||||
guard expect(elementName, named: .productsDb, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
inside = .productsDb
|
||||
case .productsDb:
|
||||
guard expect(elementName, named: .versions, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
inside = .versions
|
||||
case .versions:
|
||||
guard expect(elementName, named: .version, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
guard let number = attributeDict["number"] else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.attributeNotFound("number"))
|
||||
return
|
||||
}
|
||||
inside = .version
|
||||
currentVersion = Version(number: number)
|
||||
case .version:
|
||||
guard let seen = expect(elementName, namedOneOf: [.latestCabLink, .releases], in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
inside = seen
|
||||
case .releases:
|
||||
guard expect(elementName, named: .release, in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
guard let build = attributeDict["build"] else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.attributeNotFound("build"))
|
||||
return
|
||||
}
|
||||
inside = .release
|
||||
currentRelease = Release(build: build)
|
||||
case .release:
|
||||
guard let seen = expect(elementName, namedOneOf: [.date, .cabLink], in: xmlParser) else {
|
||||
return
|
||||
}
|
||||
inside = seen
|
||||
default:
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
||||
}
|
||||
}
|
||||
|
||||
func parser(_ xmlParser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
|
||||
guard elementName == inside?.rawValue else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
||||
return
|
||||
}
|
||||
switch inside! {
|
||||
case .productsDb:
|
||||
inside = nil
|
||||
case .versions:
|
||||
inside = .productsDb
|
||||
case .version:
|
||||
versions.append(currentVersion!)
|
||||
currentVersion = nil
|
||||
inside = .versions
|
||||
case .latestCabLink, .releases:
|
||||
inside = .version
|
||||
case .release:
|
||||
currentVersion!.releases.append(currentRelease!)
|
||||
currentRelease = nil
|
||||
inside = .releases
|
||||
case .date, .cabLink:
|
||||
inside = .release
|
||||
}
|
||||
}
|
||||
|
||||
func parser(_ xmlParser: XMLParser, foundCharacters string: String) {
|
||||
if inside == .latestCabLink {
|
||||
currentVersion!.latestCabLink = string
|
||||
} else if inside == .date {
|
||||
currentRelease!.date = string
|
||||
} else if inside == .cabLink {
|
||||
currentRelease!.cabLink = string
|
||||
} else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.unexpectedString(string, inside?.rawValue ?? ""))
|
||||
}
|
||||
}
|
||||
|
||||
private func expect(_ elementName: String, named element: Inside, in xmlParser: XMLParser) -> Bool {
|
||||
guard elementName == element.rawValue else {
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func expect(_ elementName: String, namedOneOf elements: [Inside], in xmlParser: XMLParser) -> Inside? {
|
||||
for element in elements {
|
||||
if elementName == element.rawValue {
|
||||
return element
|
||||
}
|
||||
}
|
||||
parser(xmlParser, parseErrorOccurred: ESDCatalogError.invalidElement(elementName))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
71
Source/EULAView.swift
Normal file
71
Source/EULAView.swift
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// Copyright © 2023 Turing Software, LLC. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EULAView: View {
|
||||
let url: URL
|
||||
let onAccept: () -> Void
|
||||
@Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
|
||||
|
||||
@State private var loadedEula: NSAttributedString?
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
if let loadedEula = loadedEula {
|
||||
Text(AttributedString(loadedEula)).padding()
|
||||
} else {
|
||||
HStack(alignment: .center) {
|
||||
ProgressView().progressViewStyle(.circular).padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
loadedEula = NSAttributedString(rtf: data, documentAttributes: nil) ?? NSAttributedString(string: NSLocalizedString("Failed to load EULA.", comment: "EULAView"))
|
||||
} catch {
|
||||
loadedEula = NSAttributedString(string: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 400, minHeight: 200)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .cancellationAction) {
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
ToolbarItemGroup(placement: .confirmationAction) {
|
||||
Button {
|
||||
onAccept()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
Text("Accept")
|
||||
}.disabled(loadedEula == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EULAView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EULAView(url: URL(string: "https://www.google.com/")!) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,11 +19,22 @@ import SwiftUI
|
|||
@main
|
||||
struct Main: App {
|
||||
@StateObject private var worker = Worker()
|
||||
|
||||
@AppStorage("ShowAdvancedOptions") private var showAdvancedOptions: Bool = false
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
WindowGroup(id: "ESDConvert") {
|
||||
SimpleContentView().environmentObject(worker)
|
||||
.frame(minWidth: 500, idealWidth: 500, minHeight: 300, idealHeight: 300)
|
||||
}.handlesExternalEvents(matching: Set(["ESDConvert"]))
|
||||
|
||||
WindowGroup(id: "UUPDump") {
|
||||
ContentView().environmentObject(worker)
|
||||
.frame(minWidth: 800, minHeight: 400)
|
||||
}
|
||||
}.commands {
|
||||
SidebarCommands()
|
||||
CommandGroup(after: .sidebar) {
|
||||
Toggle("Show Advanced Options", isOn: $showAdvancedOptions)
|
||||
}
|
||||
}.handlesExternalEvents(matching: Set(["UUPDump"]))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
Source/ShowWindowButtonView.swift
Normal file
64
Source/ShowWindowButtonView.swift
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// Copyright © 2023 Turing Software, LLC. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ShowWindowButtonView<Label: View>: View {
|
||||
let id: String
|
||||
let label: () -> Label
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
if #available(macOS 13, *) {
|
||||
ShowWindowButtonViewNew(id: id, label: label)
|
||||
} else {
|
||||
Button(action: {
|
||||
open(id: id)
|
||||
dismiss()
|
||||
}, label: label)
|
||||
}
|
||||
}
|
||||
|
||||
private func open(id: String) {
|
||||
let url = URL(string: "crystalfetch://\(id)")!
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13, *)
|
||||
private struct ShowWindowButtonViewNew<Label: View>: View {
|
||||
let id: String
|
||||
let label: () -> Label
|
||||
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
openWindow(id: id)
|
||||
dismiss()
|
||||
}, label: label)
|
||||
}
|
||||
}
|
||||
|
||||
struct ShowWindowButtonView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ShowWindowButtonView(id: "UUPDump") {
|
||||
Text("All builds…")
|
||||
}
|
||||
}
|
||||
}
|
||||
257
Source/SimpleContentView.swift
Normal file
257
Source/SimpleContentView.swift
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
//
|
||||
// Copyright © 2023 Turing Software, LLC. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SimpleContentView: View {
|
||||
@EnvironmentObject private var worker: Worker
|
||||
@AppStorage("ShowAdvancedOptions") private var showAdvancedOptions: Bool = false
|
||||
@State private var isConfirmCancelShown: Bool = false
|
||||
@State private var isDownloadCompleted: Bool = false
|
||||
|
||||
@State private var windowsVersion: MCTCatalogs.Windows = .windows11
|
||||
@State private var selectedBuild: MCTCatalogs.Release?
|
||||
@State private var selected: SelectedTuple = .default
|
||||
@State private var selectedFile: ESDCatalog.File?
|
||||
@State private var selectedEula: SelectedEULA?
|
||||
|
||||
@State private var languages: [DisplayString] = []
|
||||
@State private var editions: [DisplayString] = []
|
||||
|
||||
private let dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "dd-MM-yyyy"
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Form {
|
||||
Picker("Version", selection: $windowsVersion) {
|
||||
Text("Windows® 11").tag(MCTCatalogs.Windows.windows11)
|
||||
Text("Windows® 10").tag(MCTCatalogs.Windows.windows10)
|
||||
}.pickerStyle(.radioGroup)
|
||||
buildsPicker
|
||||
Picker("Architecture", selection: $selected.architecture) {
|
||||
if hasArchitecture("ARM64") {
|
||||
Text("Apple Silicon").tag("ARM64")
|
||||
}
|
||||
if hasArchitecture("x64") {
|
||||
Text("Intel x64").tag("x64")
|
||||
}
|
||||
if hasArchitecture("x86") {
|
||||
Text("Intel x86").tag("x86")
|
||||
}
|
||||
}.pickerStyle(.radioGroup)
|
||||
Picker("Language", selection: $selected.language) {
|
||||
ForEach(languages) { language in
|
||||
Text(language.display).tag(language.id)
|
||||
}
|
||||
}
|
||||
Picker("Edition", selection: $selected.edition) {
|
||||
ForEach(editions) { edition in
|
||||
Text(edition.display).tag(edition.id)
|
||||
}
|
||||
}
|
||||
if windowsVersion == .windows10 && selected.architecture == "ARM64" {
|
||||
Text("Note: This build does not work for virtualization on Apple Silicon.")
|
||||
}
|
||||
}.disabled(worker.isBusy)
|
||||
.onChange(of: windowsVersion) { newValue in
|
||||
if selectedBuild == nil {
|
||||
worker.refreshEsdCatalog(windowsVersion: newValue)
|
||||
} else {
|
||||
selectedBuild = nil
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedBuild) { newValue in
|
||||
worker.refreshEsdCatalog(windowsVersion: windowsVersion, release: newValue)
|
||||
}
|
||||
.onChange(of: worker.esdCatalog) { _ in
|
||||
refreshList()
|
||||
}
|
||||
.onChange(of: selected) { _ in
|
||||
refreshList()
|
||||
}
|
||||
Spacer()
|
||||
HStack {
|
||||
// SwiftUI BUG: ProgressView cannot go to indeterminate mode and back
|
||||
if let progress = worker.progress {
|
||||
ProgressView(value: progress) {
|
||||
progressLabel
|
||||
} currentValueLabel: {
|
||||
Text(worker.progressStatus ?? "")
|
||||
}
|
||||
} else {
|
||||
ProgressView(value: nil as Float?) {
|
||||
progressLabel
|
||||
} currentValueLabel: {
|
||||
Text(worker.progressStatus ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
if showAdvancedOptions {
|
||||
ShowWindowButtonView(id: "UUPDump") {
|
||||
Text("All builds…")
|
||||
}.disabled(worker.isBusy)
|
||||
.help("Build custom installation for any build through UUP Dump.")
|
||||
}
|
||||
Spacer()
|
||||
if worker.isBusy {
|
||||
Button(role: .cancel) {
|
||||
isConfirmCancelShown.toggle()
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
}.keyboardShortcut(.cancelAction)
|
||||
.confirmationDialog("Are you sure you want to stop the process?", isPresented: $isConfirmCancelShown) {
|
||||
Button("Stop", role: .destructive) {
|
||||
worker.stop()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
isConfirmCancelShown = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
if let eula = selectedFile?.eula {
|
||||
selectedEula = SelectedEULA(url: URL(string: eula)!)
|
||||
} else if let selectedFile = selectedFile {
|
||||
worker.download(selectedFile)
|
||||
}
|
||||
} label: {
|
||||
Text("Download…")
|
||||
}.disabled(selectedFile == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedEula) { eula in
|
||||
EULAView(url: eula.url) {
|
||||
if let selectedFile = selectedFile {
|
||||
worker.download(selectedFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(item: $worker.lastSeenError) { lastSeenError in
|
||||
Alert(title: Text(lastSeenError.message))
|
||||
}
|
||||
.padding()
|
||||
.onAppear {
|
||||
worker.refreshCatalogUrls()
|
||||
worker.refreshEsdCatalog()
|
||||
refreshList()
|
||||
}
|
||||
.onChange(of: worker.completedDownloadUrl) { newValue in
|
||||
if newValue != nil {
|
||||
isDownloadCompleted = true
|
||||
}
|
||||
}
|
||||
.fileMover(isPresented: $isDownloadCompleted, file: worker.completedDownloadUrl) { result in
|
||||
switch result {
|
||||
case .success(let success):
|
||||
worker.finalize(isoUrl: worker.completedDownloadUrl!, destinationUrl: success)
|
||||
case .failure(let failure):
|
||||
worker.lastSeenError = Worker.ErrorMessage(message: failure.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var buildsPicker: some View {
|
||||
if let builds = worker.mctCatalogs[windowsVersion]?.releases {
|
||||
Picker("Build", selection: $selectedBuild) {
|
||||
Text("Latest").tag(nil as MCTCatalogs.Release?)
|
||||
ForEach(builds) { build in
|
||||
if let date = build.date {
|
||||
Text("\(build.build) (\(dateFormatter.string(from: date)))").tag(build)
|
||||
} else {
|
||||
Text(build.build).tag(build)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var progressLabel: some View {
|
||||
if let selectedFile = selectedFile {
|
||||
Text(selectedFile.name).font(.caption)
|
||||
} else if !worker.isBusy {
|
||||
Text("No build found.").font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
private func hasArchitecture(_ name: String) -> Bool {
|
||||
worker.esdCatalog.contains(where: { $0.architecture == name })
|
||||
}
|
||||
|
||||
private func refreshList() {
|
||||
let archFiltered = worker.esdCatalog.filter({ $0.architecture == selected.architecture })
|
||||
let languagesList = archFiltered.map({ DisplayString(id: $0.languageCode, display: $0.languagePretty )})
|
||||
languages = Set(languagesList).sorted(using: KeyPathComparator(\.display))
|
||||
let languageFiltered = archFiltered.filter({ $0.languageCode == selected.language })
|
||||
let editionsList = languageFiltered.map({ DisplayString(id: $0.edition, display: $0.editionPretty )})
|
||||
editions = Set(editionsList).sorted(using: KeyPathComparator(\.display))
|
||||
selectedFile = languageFiltered.first(where: { $0.edition == selected.edition })
|
||||
}
|
||||
}
|
||||
|
||||
private struct DisplayString: Identifiable, Hashable, Equatable {
|
||||
var id: String
|
||||
var display: String
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(display)
|
||||
}
|
||||
|
||||
static func ==(lhs: DisplayString, rhs: DisplayString) -> Bool {
|
||||
lhs.display == rhs.display
|
||||
}
|
||||
}
|
||||
|
||||
private struct SelectedTuple: Equatable {
|
||||
var architecture: String = ""
|
||||
var language: String = ""
|
||||
var edition: String = ""
|
||||
|
||||
static var `default`: SelectedTuple = {
|
||||
var tuple = SelectedTuple()
|
||||
#if arch(arm64)
|
||||
tuple.architecture = "ARM64"
|
||||
#elseif arch(x86_64)
|
||||
tuple.architecture = "x64"
|
||||
#else
|
||||
tuple.architecture = "x86"
|
||||
#endif
|
||||
tuple.language = Worker.defaultLocale ?? "en-us"
|
||||
tuple.edition = "Professional"
|
||||
return tuple
|
||||
}()
|
||||
}
|
||||
|
||||
private struct SelectedEULA: Identifiable {
|
||||
let url: URL
|
||||
|
||||
var id: URL {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
struct SimpleContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SimpleContentView()
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,11 @@ struct UUPDetails: Codable {
|
|||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||
apiVersion = try values.decode(String.self, forKey: .apiVersion)
|
||||
langList = try values.decode([String].self, forKey: .langList)
|
||||
langFancyNames = try values.decode([String: String].self, forKey: .langFancyNames)
|
||||
if langList.isEmpty {
|
||||
langFancyNames = [:]
|
||||
} else {
|
||||
langFancyNames = try values.decode([String: String].self, forKey: .langFancyNames)
|
||||
}
|
||||
updateInfo = try values.decode(UpdateInfo.self, forKey: .updateInfo)
|
||||
}
|
||||
}
|
||||
|
|
@ -70,9 +74,9 @@ extension UUPDetails {
|
|||
arch = try values.decode(String.self, forKey: .arch)
|
||||
build = try values.decode(String.self, forKey: .build)
|
||||
checkBuild = try values.decode(String.self, forKey: .checkBuild)
|
||||
sku = try values.decode(Int.self, forKey: .sku)
|
||||
created = try values.decode(Int.self, forKey: .created)
|
||||
sha256ready = try values.decode(Bool.self, forKey: .sha256ready)
|
||||
sku = try values.decodeIfPresent(Int.self, forKey: .sku) ?? 0
|
||||
created = try values.decodeIfPresent(Int.self, forKey: .created) ?? 0
|
||||
sha256ready = try values.decodeIfPresent(Bool.self, forKey: .sha256ready) ?? false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ class Worker: ObservableObject {
|
|||
@Published private(set) var progress: Float?
|
||||
@Published private(set) var progressStatus: String?
|
||||
@Published var completedDownloadUrl: URL?
|
||||
|
||||
@Published var mctCatalogs: [MCTCatalogs.Windows: MCTCatalogs.Version] = [:]
|
||||
@Published var esdCatalog: [ESDCatalog.File] = []
|
||||
|
||||
private let api = UUPDumpAPI()
|
||||
private var runningTask: (Task<Void, Never>)?
|
||||
|
|
@ -56,8 +59,17 @@ class Worker: ObservableObject {
|
|||
UserDefaults.standard.bool(forKey: "ShowServerBuilds")
|
||||
}
|
||||
|
||||
var defaultLocale: String {
|
||||
UserDefaults.standard.string(forKey: "LastSelectedLocale") ?? Locale.preferredLanguages.first?.lowercased() ?? "netural"
|
||||
static nonisolated var defaultLocale: String? {
|
||||
// There is a naming conversation required for UUP API and macOS API
|
||||
// see https://github.com/TuringSoftware/CrystalFetch/issues/2 for details
|
||||
let localeMapper = [
|
||||
"zh-hans-cn": "zh-cn"
|
||||
]
|
||||
if let preferred = UserDefaults.standard.string(forKey: "LastSelectedLocale") ?? Locale.preferredLanguages.first?.lowercased() {
|
||||
return localeMapper[preferred] ?? preferred
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func refresh(findDefault: Bool = false) {
|
||||
|
|
@ -71,10 +83,25 @@ class Worker: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func lookupPossibleLocale(fromBuildDetails buildDetails: UUPDetails, withPreferredLanguage language: String? = nil) -> String {
|
||||
var decisionLocale = language ?? Self.defaultLocale ?? "netural"
|
||||
if !buildDetails.langList.contains(decisionLocale) {
|
||||
// unable to find this locale, use the English if possible
|
||||
decisionLocale = "en-us"
|
||||
}
|
||||
if !buildDetails.langList.contains(decisionLocale) {
|
||||
// unable to find the en-us, use the first value supported if possible
|
||||
decisionLocale = buildDetails.langList.first ?? ""
|
||||
}
|
||||
// if still not possible, throw the error UNSUPPORTED_LANG from server side
|
||||
return decisionLocale
|
||||
}
|
||||
|
||||
func refreshDetails(uuid: String, language: String? = nil, _ onCompletion: @escaping (BuildDetails, BuildEditions) -> Void) {
|
||||
withBusyIndication { [self] in
|
||||
let detailsResponse = try await api.fetchDetails(for: uuid)
|
||||
let editionsResponse = try await api.fetchEditions(for: uuid, language: language ?? defaultLocale)
|
||||
let language = lookupPossibleLocale(fromBuildDetails: detailsResponse, withPreferredLanguage: language)
|
||||
let editionsResponse = try await api.fetchEditions(for: uuid, language: language)
|
||||
onCompletion(BuildDetails(from: detailsResponse), BuildEditions(from: editionsResponse))
|
||||
}
|
||||
}
|
||||
|
|
@ -144,10 +171,14 @@ class Worker: ObservableObject {
|
|||
}
|
||||
|
||||
private nonisolated func extractLine(from data: Data) -> String? {
|
||||
if let string = String(data: data, encoding: .ascii)?.filter({ $0.isASCII }) {
|
||||
if let string = String(data: data, encoding: .utf8) {
|
||||
let lines = string.split(whereSeparator: \.isNewline)
|
||||
if let line = lines.filter({ !$0.isEmpty }).last {
|
||||
return String(line)
|
||||
let stringLine = String(line)
|
||||
if let pattern = try? NSRegularExpression(pattern: "\\033\\[(0|1).*?m") {
|
||||
return pattern.stringByReplacingMatches(in: stringLine, range: NSRange(location: 0, length: stringLine.count), withTemplate: "")
|
||||
}
|
||||
return stringLine
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -155,16 +186,22 @@ class Worker: ObservableObject {
|
|||
|
||||
private func convert(files url: URL) async throws {
|
||||
let script = Bundle.main.url(forResource: "convert", withExtension: "sh")!
|
||||
try await exec(at: url, executableURL: script, "wim", ".", "1")
|
||||
completedDownloadUrl = findIso(at: url)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func execv(at currentDirectoryURL: URL?, executableURL: URL, _ args: [String]) async throws -> Int32 {
|
||||
let executablePath = Bundle.main.executableURL!.deletingLastPathComponent().path
|
||||
let process = Process()
|
||||
process.executableURL = script
|
||||
process.currentDirectoryURL = url
|
||||
process.executableURL = executableURL
|
||||
process.currentDirectoryURL = currentDirectoryURL
|
||||
process.environment = ["PATH": "\(executablePath):/usr/bin:/bin:/usr/sbin:/sbin"]
|
||||
process.arguments = ["wim", ".", "1"]
|
||||
process.arguments = args
|
||||
let outputPipe = Pipe()
|
||||
outputPipe.fileHandleForReading.readabilityHandler = { handle in
|
||||
if let line = self.extractLine(from: handle.availableData) {
|
||||
NSLog("[convert.sh stdout]: %@", line)
|
||||
NSLog("[%@ stdout]: %@", executableURL.lastPathComponent, line)
|
||||
Task { @MainActor in
|
||||
self.progressStatus = line
|
||||
}
|
||||
|
|
@ -173,7 +210,7 @@ class Worker: ObservableObject {
|
|||
let errorPipe = Pipe()
|
||||
errorPipe.fileHandleForReading.readabilityHandler = { handle in
|
||||
if let line = self.extractLine(from: handle.availableData) {
|
||||
NSLog("[convert.sh stderr]: %@", line)
|
||||
NSLog("[%@ stderr]: %@", executableURL.lastPathComponent, line)
|
||||
Task { @MainActor in
|
||||
self.lastErrorLine = line
|
||||
}
|
||||
|
|
@ -209,7 +246,19 @@ class Worker: ObservableObject {
|
|||
} onCancel: {
|
||||
process.terminate()
|
||||
}
|
||||
completedDownloadUrl = findIso(at: url)
|
||||
return process.terminationStatus
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func exec(at currentDirectoryURL: URL?, executableURL: URL, _ args: String...) async throws -> Int32 {
|
||||
try await execv(at: currentDirectoryURL, executableURL: executableURL, args)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func exec(_ args: String...) async throws -> Int32 {
|
||||
let executableURL = Bundle.main.url(forAuxiliaryExecutable: args[0])!
|
||||
let args = Array(args.dropFirst())
|
||||
return try await execv(at: nil, executableURL: executableURL, args)
|
||||
}
|
||||
|
||||
func finalize(isoUrl: URL, destinationUrl: URL) {
|
||||
|
|
@ -260,6 +309,142 @@ class Worker: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - ESD Catalog
|
||||
extension Worker {
|
||||
private var windows10CatalogUrl: URL {
|
||||
URL(string: "https://go.microsoft.com/fwlink/?LinkId=841361")!
|
||||
}
|
||||
|
||||
private var windows11CatalogUrl: URL {
|
||||
URL(string: "https://go.microsoft.com/fwlink?linkid=2156292")!
|
||||
}
|
||||
|
||||
private var worprojectUrl: URL {
|
||||
URL(string: "https://worproject.com/dldserv/esd/getversions.php")!
|
||||
}
|
||||
|
||||
func refreshCatalogUrls() {
|
||||
let cacheUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
let catalogsUrl = cacheUrl.appendingPathComponent("mctcatalogs.xml")
|
||||
withBusyIndication { [self] in
|
||||
let fm = FileManager.default
|
||||
let downloader = Downloader()
|
||||
try? fm.removeItem(at: catalogsUrl)
|
||||
await downloader.enqueue(downloadUrl: worprojectUrl, to: catalogsUrl)
|
||||
do {
|
||||
try await downloader.start()
|
||||
let data = try Data(contentsOf: catalogsUrl)
|
||||
let catalogs = try await MCTCatalogs(from: data)
|
||||
mctCatalogs = await catalogs.versions
|
||||
} catch {
|
||||
NSLog("Ignoring error while trying to fetch MCT catalogs: %@", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshEsdCatalog(windowsVersion: MCTCatalogs.Windows = .windows11, release: MCTCatalogs.Release? = nil) {
|
||||
let cacheUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
let catalogUrl = cacheUrl.appendingPathComponent("catalog.cab")
|
||||
let productsUrl = cacheUrl.appendingPathComponent("products.xml")
|
||||
withBusyIndication { [self] in
|
||||
let fm = FileManager.default
|
||||
let downloader = Downloader()
|
||||
try? fm.removeItem(at: catalogUrl)
|
||||
// if we specify a release, then use it
|
||||
if let release = release {
|
||||
await downloader.enqueue(downloadUrl: release.cabUrl, to: catalogUrl)
|
||||
try await downloader.start()
|
||||
} else {
|
||||
// next try hard coded cab url
|
||||
if windowsVersion == .windows10 {
|
||||
await downloader.enqueue(downloadUrl: windows10CatalogUrl, to: catalogUrl)
|
||||
} else {
|
||||
await downloader.enqueue(downloadUrl: windows11CatalogUrl, to: catalogUrl)
|
||||
}
|
||||
do {
|
||||
try await downloader.start()
|
||||
} catch {
|
||||
// finally, see if we got a new latest cab url
|
||||
if let url = mctCatalogs[windowsVersion]?.latestCabUrl {
|
||||
await downloader.enqueue(downloadUrl: url, to: catalogUrl)
|
||||
try await downloader.start()
|
||||
} else {
|
||||
throw error // otherwise we throw the original error
|
||||
}
|
||||
}
|
||||
}
|
||||
try await exec("cabextract", "-d", cacheUrl.path, catalogUrl.path)
|
||||
let data = try Data(contentsOf: productsUrl)
|
||||
let esd = try await ESDCatalog(from: data)
|
||||
esdCatalog = await esd.files
|
||||
}
|
||||
}
|
||||
|
||||
func download(_ file: ESDCatalog.File) {
|
||||
let cacheUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
let uuid = file.sha1.count > 0 ? file.sha1 : UUID().uuidString
|
||||
let baseUrl = cacheUrl.appendingPathComponent(uuid)
|
||||
let esdUrl = baseUrl.appendingPathComponent(file.name)
|
||||
let isoUrl = esdUrl.deletingPathExtension().appendingPathExtension("iso")
|
||||
withBusyIndication { [self] in
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: isoUrl.path) {
|
||||
completedDownloadUrl = isoUrl
|
||||
return
|
||||
}
|
||||
try? fm.removeItem(at: baseUrl)
|
||||
try fm.createDirectory(at: baseUrl, withIntermediateDirectories: true)
|
||||
let idleAssertion = requestIdleAssertion()
|
||||
defer {
|
||||
if let idleAssertion = idleAssertion {
|
||||
releaseIdleAssertion(idleAssertion)
|
||||
}
|
||||
}
|
||||
completedDownloadUrl = nil
|
||||
progress = 0.0
|
||||
progressStatus = NSLocalizedString("Starting download...", comment: "Worker")
|
||||
let downloader = Downloader()
|
||||
await downloader.enqueue(downloadUrl: URL(string: file.filePath)!, to: esdUrl)
|
||||
try await downloader.start { bytesWritten, bytesTotal in
|
||||
let written = ByteCountFormatter.string(fromByteCount: bytesWritten, countStyle: .file)
|
||||
let total = ByteCountFormatter.string(fromByteCount: bytesTotal, countStyle: .file)
|
||||
Task { @MainActor in
|
||||
self.progressStatus = String.localizedStringWithFormat(NSLocalizedString("Downloading %@ of %@...", comment: "Worker"), written, total)
|
||||
let progress = (Float(bytesWritten) / Float(bytesTotal))
|
||||
self.progress = progress > 1.0 ? 1.0 : progress
|
||||
}
|
||||
}
|
||||
progressStatus = NSLocalizedString("Converting download to ISO...", comment: "Worker")
|
||||
progress = nil
|
||||
try await convert(esd: esdUrl, to: isoUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private func convert(esd esdUrl: URL, to isoUrl: URL) async throws {
|
||||
let script = Bundle.main.url(forResource: "esd2iso", withExtension: "sh")!
|
||||
var build = buildString(from: esdUrl.lastPathComponent)
|
||||
if build.count > 32 {
|
||||
build = String(build.prefix(32))
|
||||
}
|
||||
try await exec(at: nil, executableURL: script, "-v", esdUrl.path, isoUrl.path, build)
|
||||
completedDownloadUrl = isoUrl
|
||||
}
|
||||
|
||||
private func buildString(from name: String) -> String {
|
||||
var count = 0
|
||||
for (index, ch) in name.enumerated() {
|
||||
if ch == "." {
|
||||
count += 1
|
||||
}
|
||||
if count == 3 {
|
||||
let endIndex = name.index(name.startIndex, offsetBy: index-1)
|
||||
return String(name[name.startIndex...endIndex])
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
enum WorkerError: Error {
|
||||
case conversionFailedUnknown(Int32)
|
||||
case conversionFailedMessage(String)
|
||||
|
|
|
|||
1
Source/en.lproj/Localizable.strings
Normal file
1
Source/en.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
78
Source/ja.lproj/Localizable.strings
Normal file
78
Source/ja.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
|
||||
/* Sources */
|
||||
|
||||
// ESDCatalog/ESDCatalog.swift
|
||||
"Unknown parser error." = "不明な解析エラーです。";
|
||||
"Failed to parse element '%@'" = "要素“%@”の解析に失敗しました";
|
||||
"Attribute '%@' not found" = "属性“%@”が見つかりません";
|
||||
"Unexpected string '%@' for field '%@'" = "フィールド“%2$@”に予期しない文字列“%1$@”があります";
|
||||
"Localization '%@' not found" = "ローカリゼーション“%@”が見つかりません";
|
||||
"Missing element '%@' in '%@'" = "“%2$@”に要素“%1$@”がありません";
|
||||
|
||||
// UUPDump/UUPDumpAPI.swift
|
||||
"Cannot find data from the server response." = "サーバの応答からデータが見つかりません。";
|
||||
"Error returned from server: %@" = "サーバからエラーが返されました: %@";
|
||||
|
||||
// UUPDump/PrettyString.swift
|
||||
"Intel x86" = "Intel(32ビット)";
|
||||
"Intel x64" = "Intel(64ビット)";
|
||||
"Apple Silicon" = "Appleシリコン";
|
||||
"Unknown" = "不明";
|
||||
"Development" = "Dev";
|
||||
"Beta" = "Beta";
|
||||
"Canary" = "Canary";
|
||||
"Release Preview" = "Release Preview";
|
||||
"Retail" = "Retail";
|
||||
|
||||
// Main.swift
|
||||
"Show Advanced Options" = "詳細オプションを表示";
|
||||
|
||||
// ContentView.swift
|
||||
"Prerelease Builds" = "プレリリースビルド";
|
||||
"Show unstable releases which previews upcoming features." = "今後の機能をプレビューする不安定なリリースを表示します。";
|
||||
"Server Builds" = "サーバビルド";
|
||||
"Show builds for running in a server environment." = "サーバ環境で実行するためのビルドを表示します。";
|
||||
"Simple…" = "シンプル…";
|
||||
"Build installation for the latest release through ESD conversion." = "ESD変換により最新リリースのインストールをビルドします。";
|
||||
"Refresh" = "更新";
|
||||
|
||||
// SimpleContentView.swift
|
||||
"Version" = "バージョン";
|
||||
"Windows® 11" = "Windows® 11";
|
||||
"Windows® 10" = "Windows® 10";
|
||||
"Architecture" = "アーキテクチャ";
|
||||
"Language" = "言語";
|
||||
"Edition" = "エディション";
|
||||
"Note: This build does not work for virtualization on Apple Silicon." = "注意: このビルドはAppleシリコン上の仮想化では動作しません。";
|
||||
"All builds…" = "すべてのビルド…";
|
||||
"Build custom installation for any build through UUP Dump." = "UUP dumpにより任意のビルドのカスタムインストールをビルドします。";
|
||||
"Cancel" = "キャンセル";
|
||||
"Are you sure you want to stop the process?" = "処理を中止してもよろしいですか?";
|
||||
"Stop" = "中止";
|
||||
"Download…" = "ダウンロード…";
|
||||
"No build found." = "ビルドが見つかりません。";
|
||||
|
||||
// EULAView.swift
|
||||
"Failed to load EULA." = "使用許諾契約の読み込みに失敗しました。";
|
||||
"Accept" = "同意";
|
||||
|
||||
// BuildConfigView.swift
|
||||
"Channel" = "チャネル";
|
||||
"Build" = "ビルド";
|
||||
"Created" = "作成日時";
|
||||
"Editions" = "エディション";
|
||||
"I agree that I have a valid license to use this product." = "この製品を使用するための有効なライセンスを所有していることに同意します。";
|
||||
|
||||
// BuildDetails.swift
|
||||
"Unknown Language" = "不明な言語";
|
||||
|
||||
// BuildEditions.swift
|
||||
"Unknown Edition" = "不明なエディション";
|
||||
|
||||
// Worker.swift
|
||||
"Fetching files list..." = "ファイルリストを取得中…";
|
||||
"Starting download..." = "ダウンロードを開始中…";
|
||||
"Downloading %@ of %@..." = "ダウンロード中…(%1$@ / %2$@)";
|
||||
"Converting download to ISO..." = "ダウンロードをISOに変換中…";
|
||||
"Saving ISO..." = "ISOを保存中…";
|
||||
"The conversion script failed with error code %d" = "変換スクリプトはエラーコード%dで失敗しました";
|
||||
161
Source/zh-HK.lproj/Localizable.strings
Normal file
161
Source/zh-HK.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/* No comment provided by engineer. */
|
||||
"Accept" = "接受";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"All builds…" = "所有構建⋯";
|
||||
|
||||
/* PrettyString */
|
||||
"Apple Silicon" = "Apple 晶片";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Architecture" = "架構";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Are you sure you want to stop the process?" = "要停止這個程序嗎?";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Attribute '%@' not found" = "未找到屬性「%@」";
|
||||
|
||||
/* PrettyString */
|
||||
"Beta" = "Beta";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Build" = "構建";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Build custom installation for any build through UUP Dump." = "透過 UUP Dump 為任意構建製作自定安裝映像檔。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Build installation for the latest release through ESD conversion." = "透過 ESD 轉換為最新的 Release 構建安裝映像檔。";
|
||||
|
||||
/* PrettyString */
|
||||
"Canary" = "Canary";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cancel" = "取消";
|
||||
|
||||
/* UUPDumpAPI */
|
||||
"Cannot find data from the server response." = "無法由伺服器回應找到資料。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Channel" = "頻道";
|
||||
|
||||
/* Worker */
|
||||
"Converting download to ISO..." = "正在轉換下載的資料至 ISO⋯";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Created" = "製作時間";
|
||||
|
||||
/* PrettyString */
|
||||
"Development" = "Development";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Download…" = "下載⋯";
|
||||
|
||||
/* Worker */
|
||||
"Downloading %@ of %@..." = "正在下載 %1$@ / %2$@⋯";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Edition" = "版本";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Editions" = "版本";
|
||||
|
||||
/* UUPDumpAPI */
|
||||
"Error returned from server: %@" = "伺服器返回錯誤:%@";
|
||||
|
||||
/* EULAView */
|
||||
"Failed to load EULA." = "無法載入 EULA。";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Failed to parse element '%@'" = "無法解析元素「%@」";
|
||||
|
||||
/* Worker */
|
||||
"Fetching files list..." = "正在獲取檔案清單⋯";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"I agree that I have a valid license to use this product." = "我同意我擁有使用這個產品的有效許可。";
|
||||
|
||||
/* PrettyString */
|
||||
"Intel x64" = "Intel x64";
|
||||
|
||||
/* PrettyString */
|
||||
"Intel x86" = "Intel x86";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Language" = "語言";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Localization '%@' not found" = "無法找到本地化項目「%@」";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Missing element '%@' in '%@'" = "「%2$@」中遺失元素「%1$@」";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No build found." = "無法找到構建。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Note: This build does not work for virtualization on Apple Silicon." = "注意:這個構建不適用於 Apple 晶片上的虛擬化。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Prerelease Builds" = "預先發布構建";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Refresh" = "重新整理";
|
||||
|
||||
/* PrettyString */
|
||||
"Release Preview" = "Release Preview";
|
||||
|
||||
/* PrettyString */
|
||||
"Retail" = "Retail";
|
||||
|
||||
/* Worker */
|
||||
"Saving ISO..." = "正在儲存 ISO⋯";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Server Builds" = "伺服器構建";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Show Advanced Options" = "顯示進階選項";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Show builds for running in a server environment." = "顯示用於伺服器環境運行的構建。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Show unstable releases which previews upcoming features." = "顯示不穩定的發行版本,當中可以預覽即將推出的功能。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Simple…" = "簡化⋯";
|
||||
|
||||
/* Worker */
|
||||
"Starting download..." = "正在開始下載⋯";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Stop" = "停止";
|
||||
|
||||
/* Worker */
|
||||
"The conversion script failed with error code %d" = "無法執行轉換,錯誤碼為 %d";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Unexpected string '%@' for field '%@'" = "「%2$@」欄位當中出現意外字符串「%1$@」";
|
||||
|
||||
/* PrettyString */
|
||||
"Unknown" = "未知";
|
||||
|
||||
/* BuildEditions */
|
||||
"Unknown Edition" = "未知的版本";
|
||||
|
||||
/* BuildDetails */
|
||||
"Unknown Language" = "未知的語言";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Unknown parser error." = "未知的解析器錯誤。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Version" = "版本";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Windows® 10" = "Windows® 10";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Windows® 11" = "Windows® 11";
|
||||
161
Source/zh-Hans.lproj/Localizable.strings
Normal file
161
Source/zh-Hans.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/* No comment provided by engineer. */
|
||||
"Accept" = "接受";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"All builds…" = "全部版本…";
|
||||
|
||||
/* PrettyString */
|
||||
"Apple Silicon" = "Apple 芯片";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Architecture" = "架构";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Are you sure you want to stop the process?" = "你确定要停止此进程吗?";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Attribute '%@' not found" = "属性“%@”未找到";
|
||||
|
||||
/* PrettyString */
|
||||
"Beta" = "Beta";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Build" = "构建版本";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Build custom installation for any build through UUP Dump." = "通过 UUP Dump 为任何构建版本创建自定义安装程序。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Build installation for the latest release through ESD conversion." = "通过 ESD 转换为最新版本构建安装程序。";
|
||||
|
||||
/* PrettyString */
|
||||
"Canary" = "Canary";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cancel" = "取消";
|
||||
|
||||
/* UUPDumpAPI */
|
||||
"Cannot find data from the server response." = "无法从服务器响应中找到数据。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Channel" = "通道";
|
||||
|
||||
/* Worker */
|
||||
"Converting download to ISO..." = "正在将下载转换为 ISO…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Created" = "创建时间";
|
||||
|
||||
/* PrettyString */
|
||||
"Development" = "开发";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Download…" = "下载…";
|
||||
|
||||
/* Worker */
|
||||
"Downloading %@ of %@..." = "正在下载 %1$@,共 %2$@…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Edition" = "版本";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Editions" = "版本";
|
||||
|
||||
/* UUPDumpAPI */
|
||||
"Error returned from server: %@" = "服务器返回错误:%@";
|
||||
|
||||
/* EULAView */
|
||||
"Failed to load EULA." = "无法加载 EULA (最终用户许可协议)。";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Failed to parse element '%@'" = "无法解析元素“%@”";
|
||||
|
||||
/* Worker */
|
||||
"Fetching files list..." = "获取文件列表…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"I agree that I have a valid license to use this product." = "我同意我拥有使用此产品的有效许可证。";
|
||||
|
||||
/* PrettyString */
|
||||
"Intel x64" = "Intel x64";
|
||||
|
||||
/* PrettyString */
|
||||
"Intel x86" = "Intel x86";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Language" = "语言";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Localization '%@' not found" = "本地化条目“%@”未找到";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Missing element '%@' in '%@'" = "“%2$@”中缺少元素“%1$@”";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No build found." = "未找到构建版本。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Note: This build does not work for virtualization on Apple Silicon." = "注意:此版本无法在 Apple 芯片上用于虚拟化。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Prerelease Builds" = "预发行构建版本";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Refresh" = "刷新";
|
||||
|
||||
/* PrettyString */
|
||||
"Release Preview" = "发行预览版";
|
||||
|
||||
/* PrettyString */
|
||||
"Retail" = "零售版";
|
||||
|
||||
/* Worker */
|
||||
"Saving ISO..." = "正在保存 ISO…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Server Builds" = "服务器构建版本";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Show Advanced Options" = "显示高级选项";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Show builds for running in a server environment." = "显示在服务器环境中运行的构建版本。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Show unstable releases which previews upcoming features." = "显示不稳定的发行版本,其中可预览即将推出的功能。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Simple…" = "简单模式…";
|
||||
|
||||
/* Worker */
|
||||
"Starting download..." = "开始下载…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Stop" = "停止";
|
||||
|
||||
/* Worker */
|
||||
"The conversion script failed with error code %d" = "转换脚本失败,错误代码为 %d";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Unexpected string '%@' for field '%@'" = "字段“%2$@”中存在意外字符串“%1$@”";
|
||||
|
||||
/* PrettyString */
|
||||
"Unknown" = "未知";
|
||||
|
||||
/* BuildEditions */
|
||||
"Unknown Edition" = "未知版本";
|
||||
|
||||
/* BuildDetails */
|
||||
"Unknown Language" = "未知语言";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Unknown parser error." = "未知的解析器错误。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Version" = "版本";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Windows® 10" = "Windows® 10";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Windows® 11" = "Windows® 11";
|
||||
3
Source/zh-Hant.lproj/CrystalFetch-InfoPlist.strings
Normal file
3
Source/zh-Hant.lproj/CrystalFetch-InfoPlist.strings
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/* Bundle name */
|
||||
"CFBundleName" = "CrystalFetch";
|
||||
|
||||
156
Source/zh-Hant.lproj/Localizable.strings
Normal file
156
Source/zh-Hant.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/* No comment provided by engineer. */
|
||||
"Accept" = "接受";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"All builds…" = "所有組建…";
|
||||
|
||||
/* PrettyString */
|
||||
"Apple Silicon" = "Apple Silicon";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Architecture" = "系統架構";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Are you sure you want to stop the process?" = "您確定要停止程序嗎?";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Attribute '%@' not found" = "找不到「%@」屬性";
|
||||
|
||||
/* PrettyString */
|
||||
"Beta" = "Beta版通道";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Build" = "組建";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Build custom installation for any build through UUP Dump." = "透過UUP Dump建構任何組建的自訂安裝程式。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Build installation for the latest release through ESD conversion." = "透過ESD轉換建構最新發行版本的安裝程式。";
|
||||
|
||||
/* PrettyString */
|
||||
"Canary" = "Canary版通道";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cancel" = "取消";
|
||||
|
||||
/* UUPDumpAPI */
|
||||
"Cannot find data from the server response." = "從伺服器回應中無法找到資料。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Channel" = "頻道";
|
||||
|
||||
/* Worker */
|
||||
"Converting download to ISO..." = "正在將下載的資料轉換成ISO……";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Created" = "建立於";
|
||||
|
||||
/* PrettyString */
|
||||
"Development" = "Dev版通道";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Download…" = "下載…";
|
||||
|
||||
/* Worker */
|
||||
"Downloading %@ of %@..." = "已下載%1$@(共%2$@)……";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Edition" = "版本";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Editions" = "版本";
|
||||
|
||||
/* UUPDumpAPI */
|
||||
"Error returned from server: %@" = "伺服器回傳錯誤:%@";
|
||||
|
||||
/* EULAView */
|
||||
"Failed to load EULA." = "無法載入EULA。";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Failed to parse element '%@'" = "無法解析「%@」元素";
|
||||
|
||||
/* Worker */
|
||||
"Fetching files list..." = "正在抓取檔案清單……";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"I agree that I have a valid license to use this product." = "我確定我有使用本產品的合法授權。";
|
||||
|
||||
/* PrettyString */
|
||||
"Intel x64" = "Intel x64";
|
||||
|
||||
/* PrettyString */
|
||||
"Intel x86" = "Intel x86";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Language" = "語言";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Localization '%@' not found" = "找不到「%@」本地化資料";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Missing element '%@' in '%@'" = "「%2$@」中缺少「%1$@」元素";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No build found." = "找不到組建。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Prerelease Builds" = "預先發布組建";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Refresh" = "重新整理";
|
||||
|
||||
/* PrettyString */
|
||||
"Release Preview" = "發行預覽通道";
|
||||
|
||||
/* PrettyString */
|
||||
"Retail" = "零售版本通道";
|
||||
|
||||
/* Worker */
|
||||
"Saving ISO..." = "正在儲存ISO……";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Server Builds" = "伺服器組建";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Show builds for running in a server environment." = "顯示用來在伺服器環境運作的組建。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Show unstable releases which previews upcoming features." = "顯示不穩定的發行版本,可以預覽即將推出的功能。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Simple…" = "簡單…";
|
||||
|
||||
/* Worker */
|
||||
"Starting download..." = "開始下載……";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Stop" = "停止";
|
||||
|
||||
/* Worker */
|
||||
"The conversion script failed with error code %d" = "轉換文稿執行失敗,錯誤碼 %d";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Unexpected string '%@' for field '%@'" = "「%2$@」欄位中的「%1$@」字串不符預期";
|
||||
|
||||
/* PrettyString */
|
||||
"Unknown" = "不明";
|
||||
|
||||
/* BuildEditions */
|
||||
"Unknown Edition" = "不明版本";
|
||||
|
||||
/* BuildDetails */
|
||||
"Unknown Language" = "不明語言";
|
||||
|
||||
/* ESDCatalog */
|
||||
"Unknown parser error." = "不明的解析器錯誤。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Version" = "版本";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Windows® 10" = "Windows® 10";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Windows® 11" = "Windows® 11";
|
||||
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit ae44786dcd57c6668233c51fa1c6e3a60fd459cd
|
||||
Subproject commit 4baa0ab7b8ad5c7f0a109e22e9a6c8621a7edd42
|
||||
Loading…
Add table
Reference in a new issue