Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d16a6ffb1 | ||
|
|
95658072e7 | ||
|
|
7eeb09b1ec | ||
|
|
e6f21c13b0 |
6
.github/workflows/release.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
|||||||
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
|
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
|
||||||
sed -i '' -e "s/GITHUB_CI_VERSION/$CLEAN_TAG/g" Sources/Config/Config.xcconfig
|
sed -i '' -e "s/GITHUB_CI_VERSION/$CLEAN_TAG/g" Sources/Config/Config.xcconfig
|
||||||
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
||||||
- name: Build
|
- name: Build
|
||||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||||
- name: Create ZIP
|
- name: Create ZIP
|
||||||
@@ -77,13 +77,13 @@ jobs:
|
|||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v2
|
||||||
with:
|
with:
|
||||||
subject-name: "Secretive.zip"
|
subject-name: "Secretive.zip"
|
||||||
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
subject-digest: ${{ steps.upload.outputs.artifact-digest }}
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
run: |
|
run: |
|
||||||
sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md
|
sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md
|
||||||
sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md
|
sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md
|
||||||
gh release create $TAG_NAME -d -F .github/templates/release.md
|
gh release create $TAG_NAME -d -F .github/templates/release.md
|
||||||
gh release upload $TAG_NAME Secretive.zip
|
gh release upload Secretive.zip
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAG_NAME: ${{ github.ref }}
|
TAG_NAME: ${{ github.ref }}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
11
README.md
@@ -1,7 +1,8 @@
|
|||||||
# Secretive [](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml) 
|
# Secretive [](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml) 
|
||||||
|
|
||||||
|
|
||||||
Secretive is an app for protecting and managing SSH keys with the Secure Enclave.
|
Secretive is an app for storing and managing SSH keys in the Secure Enclave. It is inspired by the [sekey project](https://github.com/sekey/sekey), but rewritten in Swift with no external dependencies and with a handy native management app.
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="/.github/readme/app-dark.png">
|
<source media="(prefers-color-scheme: dark)" srcset="/.github/readme/app-dark.png">
|
||||||
<img src="/.github/readme/app-light.png" alt="Screenshot of Secretive" width="600">
|
<img src="/.github/readme/app-light.png" alt="Screenshot of Secretive" width="600">
|
||||||
@@ -61,11 +62,3 @@ Because secrets in the Secure Enclave are not exportable, they are not able to b
|
|||||||
## Security
|
## Security
|
||||||
|
|
||||||
Secretive's security policy is detailed in [SECURITY.md](SECURITY.md). To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)
|
Secretive's security policy is detailed in [SECURITY.md](SECURITY.md). To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)
|
||||||
|
|
||||||
## Acknowledgements
|
|
||||||
|
|
||||||
### sekey
|
|
||||||
Secretive was inspired by the [sekey project](https://github.com/sekey/sekey).
|
|
||||||
|
|
||||||
### Localization
|
|
||||||
Secretive is localized to many languages by a generous team of volunteers. To learn more, see [LOCALIZING.md](LOCALIZING.md). Secretive's localization workflow is generously provided by [Crowdin](https://crowdin.com).
|
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
CI_VERSION = GITHUB_CI_VERSION
|
CI_VERSION = GITHUB_CI_VERSION
|
||||||
CI_BUILD_NUMBER = GITHUB_BUILD_NUMBER
|
CI_BUILD_NUMBER = GITHUB_BUILD_NUMBER
|
||||||
CI_BUILD_LINK = GITHUB_BUILD_URL
|
|
||||||
|
|||||||
@@ -177,73 +177,6 @@
|
|||||||
"value" : ""
|
"value" : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"shouldTranslate" : false
|
|
||||||
},
|
|
||||||
"**%@** (%@)" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "**%1$@** (%2$@)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shouldTranslate" : false
|
|
||||||
},
|
|
||||||
"about_build_log_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Build Log"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"about_menu_bar_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "About Secretive"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"about_open_source_notice" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Secretive is Open Source and MIT Licensed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"about_thanks" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Special thanks our [Contributors](%1$(contributorsLink)@) and [Sponsors](%2$(sponsorsLink)@)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"about_view_on_github_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "View on GitHub"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent_details_could_not_start_error" : {
|
"agent_details_could_not_start_error" : {
|
||||||
@@ -383,8 +316,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Secretive не получлось запустить SecretAgent. Пожалуйста, попробуйте перезапустить Ваш Mac, и если это не поможет – создайте issue на GitHub."
|
"value" : "Secretive was unable to get SecretAgent to launch. Please try restarting your Mac, and if that doesn't work, file an issue on GitHub."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -568,8 +501,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Отключить агент"
|
"value" : "Disable Agent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -753,8 +686,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Перезапустить агент"
|
"value" : "Restart Agent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -938,8 +871,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Запущено с"
|
"value" : "Running Since"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -1123,8 +1056,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Путь к сокету"
|
"value" : "Socket Path"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -1308,8 +1241,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Запустить агент"
|
"value" : "Start Agent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -1493,8 +1426,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Агент запускается"
|
"value" : "Starting Agent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -1678,8 +1611,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Версия"
|
"value" : "Version"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -1863,8 +1796,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "SecretAgent это процесс, который работает в фоне чтобы подписывать запросы. Так Вам не придется все время держать Secretive открытым.\n\n**Secretive не сможет нормально функционировать, пока агент не установлен и не запущен.**"
|
"value" : "SecretAgent is a process that runs in the background to sign requests, so you don't need to keep Secretive open all the time.\n\n**Secretive will not be able to function properly unless the agent is installed and running.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -2788,8 +2721,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Расположение Secret Agent"
|
"value" : "Secret Agent Location"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -3206,23 +3139,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app_not_in_applications_notice_cancel_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Later"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ru" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Позже"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"app_not_in_applications_notice_detail_description" : {
|
"app_not_in_applications_notice_detail_description" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3408,23 +3324,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app_not_in_applications_notice_quit_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Quit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ru" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Выйти"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"app_not_in_applications_notice_title" : {
|
"app_not_in_applications_notice_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -4675,8 +4574,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Расширенное"
|
"value" : "Advanced"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -4860,8 +4759,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Если Вы сделаете _любые_ изменения в Ваших настройках биометрии, включая добавление нового отпечатка пальца, этот ключ перестанет быть доступным."
|
"value" : "If you change your biometric settings in _any way_, including adding a new fingerprint, this key will no longer be accessible."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -5415,8 +5314,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Это отображается в конце Вашего публичного ключа. Как правило, это email адрес."
|
"value" : "This shows at the end of your public key. It’s usually an email address."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -5600,8 +5499,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Принадлежность ключа"
|
"value" : "Key Attribution"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -5785,8 +5684,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Тип ключа"
|
"value" : "Key Type"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -5833,17 +5732,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"create_secret_key_type_macOS_update_required_label" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Unavailable on this version of macOS"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"create_secret_mldsa_warning" : {
|
"create_secret_mldsa_warning" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -5981,8 +5869,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Внимание: Ключи ML-DSA совсем новые, и не пока поддерживаются многими серверами. Пожалуйста удостоверьтесь, что используемый с этим ключом сервер поддерживает ключи ML-DSA."
|
"value" : "Warning: ML-DSA keys are very new, and not supported by many servers yet. Please verify the server you'll be using this key for accepts ML-DSA keys."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -6537,7 +6425,7 @@
|
|||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Аутентификация не потребуется пока Ваш Mac разблокирован, но Вы получите уведомление, если секрет будет использован."
|
"value" : "Аутентификация не потребуется пока Ваш Mac разблокирован, но Вы получите уведомление, если секрет был использован."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -6906,8 +6794,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Уровень защиты"
|
"value" : "Protection Level"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -7091,8 +6979,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Требовать аутентификацию с текущим набором биометрии."
|
"value" : "Require authentication with current set of biometrics."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -7276,8 +7164,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Текущая биометрия"
|
"value" : "Current Biometrics"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -9496,8 +9384,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Похоже, что Вы могли недавно обновить MacOS. Иногда из-за этого возникают проблемы с Secure Enclave, и возможно Вам потребуется перезапустить Ваш Mac, чтобы их исправить."
|
"value" : "It looks like you may have recently updated macOS. Sometimes this puts the Secure Enclave into a weird state, and you might need to reboot your Mac before things start working again."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -9681,8 +9569,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Не хватает секретов?"
|
"value" : "Missing Secrets?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -10779,8 +10667,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Добавить следующее:"
|
"value" : "Add This:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -10964,8 +10852,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Приложения"
|
"value" : "Apps"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11149,8 +11037,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Существует список инструкций, поддерживаемый сообществом, к приложениям на GitHub. Если нужное Вам приложение не поддерживается, создайте issue, возможно сообщество сможет помочь Вам."
|
"value" : "There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11334,8 +11222,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Существует список shell-скриптов, поддерживаемый сообществом на GitHub. Если нужный Вам shell-скрипт не поддерживается, создайте issue, возможно сообщество сможет помочь Вам."
|
"value" : "There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11519,8 +11407,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Вам потребуется создать секрет перед настройкой этого действия."
|
"value" : "You'll need to create a Secret before configuring this action."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11704,8 +11592,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Настроить используя секрет"
|
"value" : "Configure Using Secret"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11889,8 +11777,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Без секрета"
|
"value" : "No Secret"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12074,8 +11962,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "С секретом"
|
"value" : "Secret"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12259,8 +12147,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Вы можете настроить более чем один инструмент. Обычно это не вызывает конфликтов при работе."
|
"value" : "You can configure more than one tool, they generally won't interfere with each other."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12444,8 +12332,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Начало работы"
|
"value" : "Getting Started"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12629,8 +12517,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Интеграции"
|
"value" : "Integrations"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12814,8 +12702,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Если Вы хотите подписывать ваши git коммиты, настройте подпись Git."
|
"value" : "If you're trying to sign your git commits, set up Git Signing."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12999,8 +12887,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Если Вы хотите наладить работу Secretive с Вашими скриптами в Терминале, настройте Ваш Терминал."
|
"value" : "If you're trying to configure anything your command line runs to use Secretive, configure your shell."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13184,8 +13072,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Если Вы не знаете, какой Терминал используете и не меняли его раннее, то скорее всего Вы используете `%(shellName)@`."
|
"value" : "If you don't known what shell you use and haven't changed it, you're probably using `%(shellName)@`."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13369,8 +13257,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Если Вы хотите аутентифицироваться с SSH сервером, или с сервисом как GitHub через SSH, настройте Ваш SSH клиент."
|
"value" : "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13554,8 +13442,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Настройка инструментов для Secretive"
|
"value" : "Configuring Tools for Secretive"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13739,8 +13627,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Большинство инструментов попробуют найти SSH ключи в `~/.ssh`. Для того, чтобы использовать Secretive, нам потребуется настроить эти инструменты, чтобы они работали с Secretive."
|
"value" : "Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13924,8 +13812,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Что мне следует настроить?"
|
"value" : "What Should I Configure?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -14109,8 +13997,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Возможно, директория ~/.gitallowedsigners не существует. Вам потребуется создать ее."
|
"value" : "~/.gitallowedsigners probably does not exist. You'll need to create it."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -14480,8 +14368,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Интеграции…"
|
"value" : "Integrations…"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -14665,8 +14553,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Другое"
|
"value" : "Other"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -14850,8 +14738,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "другое"
|
"value" : "other"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -15035,8 +14923,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Файл конфигурации"
|
"value" : "Configuration File"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -15220,7 +15108,7 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Shell"
|
"value" : "Shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -15405,8 +15293,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Вы можете настроить SSH так, чтобы использовался конкретный ключ для выбранного хоста. Посмотрите документацию в интернете, чтобы узнать подробности."
|
"value" : "You can tell SSH to use a specific key for a given host. See the web documentation for more details."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -15590,8 +15478,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Система"
|
"value" : "System"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -16147,8 +16035,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Подпись Git"
|
"value" : "Git Signing"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -16704,8 +16592,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Посмотреть на GitHub"
|
"value" : "View on GitHub"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -16889,8 +16777,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Найти документацию в интернете"
|
"value" : "View Documentation on Web"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -17074,8 +16962,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Если любой раздел (как [user]) уже существует, просто добавьте новые записи в него."
|
"value" : "If any section (like [user]) already exists, just add the entries in the existing section."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -18002,7 +17890,7 @@
|
|||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Не разблокировать"
|
"value" : "Не разблокировывать"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -18186,8 +18074,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Открыть в Finder"
|
"value" : "Reveal in Finder"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -19481,7 +19369,7 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Secure Enclave"
|
"value" : "Secure Enclave"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -19846,7 +19734,7 @@
|
|||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Это вспомогательное приложение назвается **Secret Agent**, Вы можете видеть его в Activity Monitor время от времени."
|
"value" : "Это вспомогательное приложение назвается **Secret Agent**, Вы можете наблюдать его в Activity Monitor время от времени."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -20401,7 +20289,7 @@
|
|||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Настроить агент"
|
"value" : "Настроить агента"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -20585,8 +20473,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Готово"
|
"value" : "Done"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -20770,8 +20658,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Настроить"
|
"value" : "Configure"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -20955,8 +20843,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Настройте Ваши инструменты для работы с Secretive"
|
"value" : "Tell the tools you use how to talk to Secretive."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -21140,8 +21028,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Настроить интеграции"
|
"value" : "Configure Integrations"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -21880,8 +21768,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Готово"
|
"value" : "Done"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -24847,7 +24735,7 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Secretive %1$(updateName)@"
|
"value" : "Secretive %1$(updateName)@"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -25032,8 +24920,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Скачать последнюю ночную сборку"
|
"value" : "Download Latest Nightly Build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
/// A release is a representation of a downloadable update.
|
/// A release is a representation of a downloadable update.
|
||||||
public struct Release: Codable, Sendable, Hashable {
|
public struct Release: Codable, Sendable {
|
||||||
|
|
||||||
/// The user-facing name of the release. Typically "Secretive 1.2.3"
|
/// The user-facing name of the release. Typically "Secretive 1.2.3"
|
||||||
public let name: String
|
public let name: String
|
||||||
@@ -16,8 +15,6 @@ public struct Release: Codable, Sendable, Hashable {
|
|||||||
/// A user-facing description of the contents of the update.
|
/// A user-facing description of the contents of the update.
|
||||||
public let body: String
|
public let body: String
|
||||||
|
|
||||||
public let attributedBody: AttributedString
|
|
||||||
|
|
||||||
/// Initializes a Release.
|
/// Initializes a Release.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - name: The user-facing name of the release.
|
/// - name: The user-facing name of the release.
|
||||||
@@ -29,56 +26,6 @@ public struct Release: Codable, Sendable, Hashable {
|
|||||||
self.prerelease = prerelease
|
self.prerelease = prerelease
|
||||||
self.html_url = html_url
|
self.html_url = html_url
|
||||||
self.body = body
|
self.body = body
|
||||||
self.attributedBody = AttributedString(_markdown: body)
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(_ release: GitHubRelease) {
|
|
||||||
self.name = release.name
|
|
||||||
self.prerelease = release.prerelease
|
|
||||||
self.html_url = release.html_url
|
|
||||||
self.body = release.body
|
|
||||||
self.attributedBody = AttributedString(_markdown: release.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct GitHubRelease: Codable, Sendable {
|
|
||||||
let name: String
|
|
||||||
let prerelease: Bool
|
|
||||||
let html_url: URL
|
|
||||||
let body: String
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate extension AttributedString {
|
|
||||||
|
|
||||||
init(_markdown markdown: String) {
|
|
||||||
let split = markdown.split(whereSeparator: \.isNewline)
|
|
||||||
let lines = split
|
|
||||||
.compactMap {
|
|
||||||
try? AttributedString(markdown: String($0), options: .init(allowsExtendedAttributes: true, interpretedSyntax: .full))
|
|
||||||
}
|
|
||||||
.map { (string: AttributedString) in
|
|
||||||
guard case let .header(level) = string.runs.first?.presentationIntent?.components.first?.kind else { return string }
|
|
||||||
return AttributedString("\n") + string
|
|
||||||
.transformingAttributes(\.font) { font in
|
|
||||||
font.value = switch level {
|
|
||||||
case 2: .headline.bold()
|
|
||||||
case 3: .headline
|
|
||||||
default: .subheadline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.transformingAttributes(\.underlineStyle) { underline in
|
|
||||||
underline.value = switch level {
|
|
||||||
case 2: .single
|
|
||||||
default: .none
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+ AttributedString("\n")
|
|
||||||
}
|
|
||||||
self = lines.reduce(into: AttributedString()) { partialResult, next in
|
|
||||||
partialResult.append(next)
|
|
||||||
partialResult.append(AttributedString("\n"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiab
|
|||||||
private let _create: @Sendable (String, Attributes) async throws -> AnySecret
|
private let _create: @Sendable (String, Attributes) async throws -> AnySecret
|
||||||
private let _delete: @Sendable (AnySecret) async throws -> Void
|
private let _delete: @Sendable (AnySecret) async throws -> Void
|
||||||
private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void
|
private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void
|
||||||
private let _supportedKeyTypes: @Sendable () -> KeyAvailability
|
private let _supportedKeyTypes: @Sendable () -> [KeyType]
|
||||||
|
|
||||||
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
||||||
_create = { AnySecret(try await secretStore.create(name: $0, attributes: $1)) }
|
_create = { AnySecret(try await secretStore.create(name: $0, attributes: $1)) }
|
||||||
@@ -87,7 +87,7 @@ public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiab
|
|||||||
try await _update(secret, name, attributes)
|
try await _update(secret, name, attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var supportedKeyTypes: KeyAvailability {
|
public var supportedKeyTypes: [KeyType] {
|
||||||
_supportedKeyTypes()
|
_supportedKeyTypes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,9 @@ public final class PublicKeyFileStoreController: Sendable {
|
|||||||
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
|
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
|
||||||
logger.log("Writing public keys to disk")
|
logger.log("Writing public keys to disk")
|
||||||
if clear {
|
if clear {
|
||||||
let validPaths = Set(secrets.map { publicKeyPath(for: $0) })
|
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
||||||
.union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
|
||||||
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
|
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
|
||||||
let fullPathContents = contentsOfDirectory.map { directory.appending(path: $0).path() }
|
let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" }
|
||||||
|
|
||||||
let untracked = Set(fullPathContents)
|
let untracked = Set(fullPathContents)
|
||||||
.subtracting(validPaths)
|
.subtracting(validPaths)
|
||||||
|
|||||||
@@ -62,37 +62,10 @@ public protocol SecretStoreModifiable<SecretType>: SecretStore {
|
|||||||
/// - attributes: The new attributes for the secret.
|
/// - attributes: The new attributes for the secret.
|
||||||
func update(secret: SecretType, name: String, attributes: Attributes) async throws
|
func update(secret: SecretType, name: String, attributes: Attributes) async throws
|
||||||
|
|
||||||
var supportedKeyTypes: KeyAvailability { get }
|
var supportedKeyTypes: [KeyType] { get }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct KeyAvailability: Sendable {
|
|
||||||
|
|
||||||
public let available: [KeyType]
|
|
||||||
public let unavailable: [UnavailableKeyType]
|
|
||||||
|
|
||||||
public init(available: [KeyType], unavailable: [UnavailableKeyType]) {
|
|
||||||
self.available = available
|
|
||||||
self.unavailable = unavailable
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct UnavailableKeyType: Sendable {
|
|
||||||
public let keyType: KeyType
|
|
||||||
public let reason: Reason
|
|
||||||
|
|
||||||
public init(keyType: KeyType, reason: Reason) {
|
|
||||||
self.keyType = keyType
|
|
||||||
self.reason = reason
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Reason: Sendable {
|
|
||||||
case macOSUpdateRequired
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extension NSNotification.Name {
|
extension NSNotification.Name {
|
||||||
|
|
||||||
// Distributed notification that keys were modified out of process (ie, that the management tool added/removed secrets)
|
// Distributed notification that keys were modified out of process (ie, that the management tool added/removed secrets)
|
||||||
|
|||||||
@@ -50,16 +50,16 @@ extension SecureEnclave {
|
|||||||
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
|
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
|
||||||
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
|
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
|
||||||
logger.log("Skipping \(name), public key already present. Marking as migrated.")
|
logger.log("Skipping \(name), public key already present. Marking as migrated.")
|
||||||
markMigrated(secret: secret, oldID: id)
|
try markMigrated(secret: secret, oldID: id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
logger.log("Migrating \(name).")
|
logger.log("Migrating \(name).")
|
||||||
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
|
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
|
||||||
logger.log("Migrated \(name).")
|
logger.log("Migrated \(name).")
|
||||||
markMigrated(secret: secret, oldID: id)
|
try markMigrated(secret: secret, oldID: id)
|
||||||
migratedAny = true
|
migratedAny = true
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to migrate \(name): \(error.localizedDescription).")
|
logger.error("Failed to migrate \(name): \(error).")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if migratedAny {
|
if migratedAny {
|
||||||
@@ -69,10 +69,10 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
public func markMigrated(secret: Secret, oldID: Data) {
|
public func markMigrated(secret: Secret, oldID: Data) throws {
|
||||||
let updateQuery = KeychainDictionary([
|
let updateQuery = KeychainDictionary([
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrApplicationLabel: oldID
|
kSecAttrApplicationLabel: secret.id
|
||||||
])
|
])
|
||||||
|
|
||||||
let newID = oldID + Constants.migrationMagicNumber
|
let newID = oldID + Constants.migrationMagicNumber
|
||||||
@@ -82,7 +82,7 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
let status = SecItemUpdate(updateQuery, updatedAttributes)
|
let status = SecItemUpdate(updateQuery, updatedAttributes)
|
||||||
if status != errSecSuccess {
|
if status != errSecSuccess {
|
||||||
logger.warning("Failed to mark \(secret.name) as migrated: \(status).")
|
throw KeychainError(statusCode: status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,22 +186,17 @@ extension SecureEnclave {
|
|||||||
await reloadSecrets()
|
await reloadSecrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
public let supportedKeyTypes: KeyAvailability = {
|
public var supportedKeyTypes: [KeyType] {
|
||||||
let macOS26Keys: [KeyType] = [.mldsa65, .mldsa87]
|
if #available(macOS 26, *) {
|
||||||
let isAtLeastMacOS26 = if #available(macOS 26, *) {
|
[
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
return KeyAvailability(
|
|
||||||
available: [
|
|
||||||
.ecdsa256,
|
.ecdsa256,
|
||||||
] + (isAtLeastMacOS26 ? macOS26Keys : []),
|
.mldsa65,
|
||||||
unavailable: (isAtLeastMacOS26 ? [] : macOS26Keys).map {
|
.mldsa87,
|
||||||
KeyAvailability.UnavailableKeyType(keyType: $0, reason: .macOSUpdateRequired)
|
]
|
||||||
}
|
} else {
|
||||||
)
|
[.ecdsa256]
|
||||||
}()
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ public final class XPCServiceDelegate: NSObject, NSXPCListenerDelegate {
|
|||||||
if let error = error as? Codable & Error {
|
if let error = error as? Codable & Error {
|
||||||
reply(nil, NSError(error))
|
reply(nil, NSError(error))
|
||||||
} else {
|
} else {
|
||||||
// Sending cast directly tries to serialize it and crashes XPCEncoder.
|
reply(nil, error)
|
||||||
let cast = error as NSError
|
|
||||||
reply(nil, NSError(domain: cast.domain, code: cast.code, userInfo: [NSLocalizedDescriptionKey: error.localizedDescription]))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,18 @@ import Foundation
|
|||||||
import SecretAgentKit
|
import SecretAgentKit
|
||||||
import Brief
|
import Brief
|
||||||
import XPCWrappers
|
import XPCWrappers
|
||||||
import OSLog
|
|
||||||
|
|
||||||
/// Delegates all agent input parsing to an XPC service which wraps OpenSSH
|
/// Delegates all agent input parsing to an XPC service which wraps OpenSSH
|
||||||
public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
|
public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "XPCAgentInputParser")
|
|
||||||
private let session: XPCTypedSession<SSHAgent.Request, SSHAgentInputParser.AgentParsingError>
|
private let session: XPCTypedSession<SSHAgent.Request, SSHAgentInputParser.AgentParsingError>
|
||||||
|
|
||||||
public init() async throws {
|
public init() async throws {
|
||||||
logger.debug("Creating XPCAgentInputParser")
|
|
||||||
session = try await XPCTypedSession(serviceName: "com.maxgoedjen.Secretive.SecretAgentInputParser", warmup: true)
|
session = try await XPCTypedSession(serviceName: "com.maxgoedjen.Secretive.SecretAgentInputParser", warmup: true)
|
||||||
logger.debug("XPCAgentInputParser is warmed up.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func parse(data: Data) async throws -> SSHAgent.Request {
|
public func parse(data: Data) async throws -> SSHAgent.Request {
|
||||||
logger.debug("Parsing input")
|
try await session.send(data)
|
||||||
defer { logger.debug("Parsed input") }
|
|
||||||
return try await session.send(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */; };
|
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */; };
|
||||||
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF642780081B00DF2006 /* SmartCardSecretKit */; };
|
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF642780081B00DF2006 /* SmartCardSecretKit */; };
|
||||||
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
||||||
|
5008C2402E52792400507AC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
|
||||||
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
||||||
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
|
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
|
||||||
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
@@ -35,11 +36,13 @@
|
|||||||
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
||||||
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
|
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
|
||||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
|
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
|
||||||
|
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
|
||||||
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
||||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
||||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
||||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
||||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
||||||
|
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
|
||||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
|
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
|
||||||
50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
50692D282E6FDB8D0043C7BB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692D242E6FDB8D0043C7BB /* main.swift */; };
|
50692D282E6FDB8D0043C7BB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692D242E6FDB8D0043C7BB /* main.swift */; };
|
||||||
@@ -70,10 +73,6 @@
|
|||||||
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
|
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
|
||||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
|
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
|
||||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
|
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
|
||||||
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; };
|
|
||||||
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; };
|
|
||||||
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
|
|
||||||
50E4C4C92E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -198,6 +197,7 @@
|
|||||||
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
|
50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
|
||||||
50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
50617D8623FCE48E0099B055 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
50617D8923FCE48E0099B055 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
50617D8923FCE48E0099B055 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
50617D8E23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
50617D8E23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
50617D8F23FCE48E0099B055 /* Secretive.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Secretive.entitlements; sourceTree = "<group>"; };
|
50617D8F23FCE48E0099B055 /* Secretive.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Secretive.entitlements; sourceTree = "<group>"; };
|
||||||
@@ -205,6 +205,7 @@
|
|||||||
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; };
|
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; };
|
||||||
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
|
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
|
||||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
|
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
|
||||||
|
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
|
||||||
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
|
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
|
||||||
50692BA52E6D5CC90043C7BB /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
|
50692BA52E6D5CC90043C7BB /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
|
||||||
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveUpdater.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
|
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveUpdater.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -238,9 +239,6 @@
|
|||||||
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
|
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
|
||||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
|
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
|
||||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
|
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
|
||||||
50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowBackgroundStyle.swift; sourceTree = "<group>"; };
|
|
||||||
50E4C4C22E7765DF00C73783 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
|
||||||
50E4C4C72E777E4200C73783 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -295,16 +293,15 @@
|
|||||||
path = Helpers;
|
path = Helpers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
504788ED2E681EB200B4556F /* Modifiers */ = {
|
504788ED2E681EB200B4556F /* Styles */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */,
|
|
||||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
|
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
|
||||||
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
|
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
|
||||||
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */,
|
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */,
|
||||||
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
|
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
|
||||||
);
|
);
|
||||||
path = Modifiers;
|
path = Styles;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
504788EE2E681EC300B4556F /* Secrets */ = {
|
504788EE2E681EC300B4556F /* Secrets */ = {
|
||||||
@@ -338,7 +335,6 @@
|
|||||||
504788F02E681F0100B4556F /* Views */ = {
|
504788F02E681F0100B4556F /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
50E4C4C22E7765DF00C73783 /* AboutView.swift */,
|
|
||||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
||||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
||||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||||
@@ -379,10 +375,11 @@
|
|||||||
508A58B0241ED1C40069DC07 /* Views */,
|
508A58B0241ED1C40069DC07 /* Views */,
|
||||||
508A58B1241ED1EA0069DC07 /* Controllers */,
|
508A58B1241ED1EA0069DC07 /* Controllers */,
|
||||||
50033AC427813F1C00253856 /* Helpers */,
|
50033AC427813F1C00253856 /* Helpers */,
|
||||||
|
50617D8623FCE48E0099B055 /* Assets.xcassets */,
|
||||||
50617D8E23FCE48E0099B055 /* Info.plist */,
|
50617D8E23FCE48E0099B055 /* Info.plist */,
|
||||||
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
|
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
|
||||||
50E4C4C72E777E4200C73783 /* AppIcon.icon */,
|
|
||||||
50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
|
50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
|
||||||
|
506772C62424784600034DED /* Credits.rtf */,
|
||||||
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
|
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
|
||||||
50617D8823FCE48E0099B055 /* Preview Content */,
|
50617D8823FCE48E0099B055 /* Preview Content */,
|
||||||
);
|
);
|
||||||
@@ -435,7 +432,7 @@
|
|||||||
children = (
|
children = (
|
||||||
504788EF2E681ED700B4556F /* Configuration */,
|
504788EF2E681ED700B4556F /* Configuration */,
|
||||||
504788EE2E681EC300B4556F /* Secrets */,
|
504788EE2E681EC300B4556F /* Secrets */,
|
||||||
504788ED2E681EB200B4556F /* Modifiers */,
|
504788ED2E681EB200B4556F /* Styles */,
|
||||||
504788F02E681F0100B4556F /* Views */,
|
504788F02E681F0100B4556F /* Views */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
@@ -646,8 +643,9 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */,
|
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */,
|
||||||
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */,
|
|
||||||
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */,
|
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */,
|
||||||
|
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */,
|
||||||
|
506772C72424784600034DED /* Credits.rtf in Resources */,
|
||||||
508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */,
|
508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -673,8 +671,8 @@
|
|||||||
50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
|
50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
|
||||||
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */,
|
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */,
|
||||||
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
|
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
|
||||||
50E4C4C92E777E4200C73783 /* AppIcon.icon in Resources */,
|
|
||||||
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
|
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
|
||||||
|
5008C2402E52792400507AC2 /* Assets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -687,9 +685,7 @@
|
|||||||
files = (
|
files = (
|
||||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
|
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
|
||||||
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
|
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
|
||||||
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */,
|
|
||||||
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
||||||
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */,
|
|
||||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||||
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
|
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
|
||||||
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
|
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
|
||||||
@@ -1049,7 +1045,7 @@
|
|||||||
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
|
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
|
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1089,7 +1085,7 @@
|
|||||||
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
|
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
|
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1131,7 +1127,7 @@
|
|||||||
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
|
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
|
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1163,7 +1159,7 @@
|
|||||||
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
|
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
|
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1193,7 +1189,7 @@
|
|||||||
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
|
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
|
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -1225,7 +1221,7 @@
|
|||||||
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
|
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 26.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
|
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
|||||||
@@ -4,18 +4,55 @@ import SecureEnclaveSecretKit
|
|||||||
import SmartCardSecretKit
|
import SmartCardSecretKit
|
||||||
import Brief
|
import Brief
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
|
||||||
|
// This is injected through .environment modifier below instead of @Entry for performance reasons (basially, restrictions around init/mainactor causing delay in loading secrets/"empty screen" blip).
|
||||||
|
@MainActor fileprivate static let _secretStoreList: SecretStoreList = {
|
||||||
|
let list = SecretStoreList()
|
||||||
|
let cryptoKit = SecureEnclave.Store()
|
||||||
|
let migrator = SecureEnclave.CryptoKitMigrator()
|
||||||
|
try? migrator.migrate(to: cryptoKit)
|
||||||
|
list.add(store: cryptoKit)
|
||||||
|
list.add(store: SmartCard.Store())
|
||||||
|
return list
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let _agentStatusChecker = AgentStatusChecker()
|
||||||
|
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
|
||||||
|
private static let _updater: any UpdaterProtocol = {
|
||||||
|
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||||
|
return Updater(checkOnLaunch: hasRunSetup)
|
||||||
|
}()
|
||||||
|
@Entry var updater: any UpdaterProtocol = _updater
|
||||||
|
|
||||||
|
private static let _justUpdatedChecker = JustUpdatedChecker()
|
||||||
|
@Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker
|
||||||
|
|
||||||
|
@MainActor var secretStoreList: SecretStoreList {
|
||||||
|
EnvironmentValues._secretStoreList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Secretive: App {
|
struct Secretive: App {
|
||||||
|
|
||||||
@Environment(\.agentStatusChecker) var agentStatusChecker
|
@Environment(\.agentStatusChecker) var agentStatusChecker
|
||||||
@Environment(\.justUpdatedChecker) var justUpdatedChecker
|
@Environment(\.justUpdatedChecker) var justUpdatedChecker
|
||||||
|
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||||
|
@State private var showingSetup = false
|
||||||
|
@State private var showingIntegrations = false
|
||||||
|
@State private var showingCreation = false
|
||||||
|
|
||||||
@SceneBuilder var body: some Scene {
|
@SceneBuilder var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
||||||
.environment(EnvironmentValues._secretStoreList)
|
.environment(EnvironmentValues._secretStoreList)
|
||||||
|
.onAppear {
|
||||||
|
if !hasRunSetup {
|
||||||
|
showingSetup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
|
||||||
guard hasRunSetup else { return }
|
guard hasRunSetup else { return }
|
||||||
agentStatusChecker.check()
|
agentStatusChecker.check()
|
||||||
if agentStatusChecker.running && justUpdatedChecker.justUpdatedBuild {
|
if agentStatusChecker.running && justUpdatedChecker.justUpdatedBuild {
|
||||||
@@ -25,52 +62,25 @@ struct Secretive: App {
|
|||||||
forceLaunchAgent()
|
forceLaunchAgent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showingIntegrations) {
|
||||||
|
IntegrationsView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.commands {
|
.commands {
|
||||||
AppCommands()
|
|
||||||
}
|
|
||||||
WindowGroup(id: String(describing: IntegrationsView.self)) {
|
|
||||||
IntegrationsView()
|
|
||||||
}
|
|
||||||
.windowResizability(.contentMinSize)
|
|
||||||
WindowGroup(id: String(describing: AboutView.self)) {
|
|
||||||
AboutView()
|
|
||||||
}
|
|
||||||
.windowStyle(.hiddenTitleBar)
|
|
||||||
.windowResizability(.contentSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Secretive {
|
|
||||||
|
|
||||||
struct AppCommands: Commands {
|
|
||||||
|
|
||||||
@Environment(\.openWindow) var openWindow
|
|
||||||
@Environment(\.openURL) var openURL
|
|
||||||
@FocusedValue(\.showCreateSecret) var showCreateSecret
|
|
||||||
|
|
||||||
var body: some Commands {
|
|
||||||
CommandGroup(replacing: .appInfo) {
|
|
||||||
Button(.aboutMenuBarTitle, systemImage: "info.circle") {
|
|
||||||
openWindow(id: String(describing: AboutView.self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CommandGroup(before: CommandGroupPlacement.appSettings) {
|
CommandGroup(before: CommandGroupPlacement.appSettings) {
|
||||||
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
|
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
|
||||||
openWindow(id: String(describing: IntegrationsView.self))
|
showingIntegrations = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CommandGroup(after: CommandGroupPlacement.newItem) {
|
CommandGroup(after: CommandGroupPlacement.newItem) {
|
||||||
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
Button(.appMenuNewSecretButton) {
|
||||||
showCreateSecret?()
|
showingCreation = true
|
||||||
}
|
}
|
||||||
.keyboardShortcut(KeyboardShortcut(KeyEquivalent("N"), modifiers: [.command, .shift]))
|
.keyboardShortcut(KeyboardShortcut(KeyEquivalent("N"), modifiers: [.command, .shift]))
|
||||||
.disabled(showCreateSecret?.isEnabled == false)
|
|
||||||
}
|
}
|
||||||
CommandGroup(replacing: .help) {
|
CommandGroup(replacing: .help) {
|
||||||
Button(.appMenuHelpButton) {
|
Button(.appMenuHelpButton) {
|
||||||
openURL(Constants.helpURL)
|
NSWorkspace.shared.open(Constants.helpURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SidebarCommands()
|
SidebarCommands()
|
||||||
@@ -103,56 +113,8 @@ extension Secretive {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private enum Constants {
|
private enum Constants {
|
||||||
static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")!
|
static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension EnvironmentValues {
|
|
||||||
|
|
||||||
// This is injected through .environment modifier below instead of @Entry for performance reasons (basially, restrictions around init/mainactor causing delay in loading secrets/"empty screen" blip).
|
|
||||||
@MainActor fileprivate static let _secretStoreList: SecretStoreList = {
|
|
||||||
let list = SecretStoreList()
|
|
||||||
let cryptoKit = SecureEnclave.Store()
|
|
||||||
let migrator = SecureEnclave.CryptoKitMigrator()
|
|
||||||
try? migrator.migrate(to: cryptoKit)
|
|
||||||
list.add(store: cryptoKit)
|
|
||||||
list.add(store: SmartCard.Store())
|
|
||||||
return list
|
|
||||||
}()
|
|
||||||
|
|
||||||
private static let _agentStatusChecker = AgentStatusChecker()
|
|
||||||
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
|
|
||||||
private static let _updater: any UpdaterProtocol = {
|
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
|
||||||
return Updater(checkOnLaunch: hasRunSetup)
|
|
||||||
}()
|
|
||||||
@Entry var updater: any UpdaterProtocol = _updater
|
|
||||||
|
|
||||||
private static let _justUpdatedChecker = JustUpdatedChecker()
|
|
||||||
@Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker
|
|
||||||
|
|
||||||
@MainActor var secretStoreList: SecretStoreList {
|
|
||||||
EnvironmentValues._secretStoreList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension FocusedValues {
|
|
||||||
@Entry var showCreateSecret: OpenSheet?
|
|
||||||
}
|
|
||||||
|
|
||||||
final class OpenSheet {
|
|
||||||
|
|
||||||
let closure: () -> Void
|
|
||||||
let isEnabled: Bool
|
|
||||||
|
|
||||||
init(isEnabled: Bool = true, closure: @escaping () -> Void) {
|
|
||||||
self.isEnabled = isEnabled
|
|
||||||
self.closure = closure
|
|
||||||
}
|
|
||||||
|
|
||||||
func callAsFunction() {
|
|
||||||
closure()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Icon-macOS-ClearDark-16x16@1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-macOS-ClearDark-16x16@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-macOS-ClearDark-32x32@1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-macOS-ClearDark-32x32@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-macOS-ClearDark-128x128@1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-macOS-ClearDark-128x128@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-macOS-ClearDark-256x256@1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-macOS-ClearDark-256x256@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-macOS-ClearDark-512x512@1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-macOS-ClearDark-1024x1024@1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 856 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 356 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 356 KiB |
6
Sources/Secretive/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Sources/Secretive/Credits.rtf
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{\rtf1\ansi\ansicpg1252\cocoartf2580
|
||||||
|
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||||
|
{\colortbl;\red255\green255\blue255;}
|
||||||
|
{\*\expandedcolortbl;;}
|
||||||
|
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
|
||||||
|
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6119\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive"}}{\fldrslt
|
||||||
|
\f0\fs24 \cf0 GitHub Repository}}
|
||||||
|
\f0\fs24 \
|
||||||
|
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
|
||||||
|
\cf0 \
|
||||||
|
{\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\
|
||||||
|
\
|
||||||
|
Special Thanks To:\
|
||||||
|
\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive/graphs/contributors"}}{\fldrslt Contributors}}:\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/0xflotus"}}{\fldrslt 0xflotus}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/aaron-trout"}}{\fldrslt Aaron Trout}}\
|
||||||
|
\pard\pardeftab720\partightenfactor0
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/EppO"}}{\fldrslt \cf0 Florent Monbillard}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/vladimyr"}}{\fldrslt Dario Vladovi\uc0\u263 }}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/lavalleeale"}}{\fldrslt Alex Lavallee}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/joshheyse"}}{\fldrslt Josh}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/diesal11"}}{\fldrslt Dylan Lundy}}\
|
||||||
|
\
|
||||||
|
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
|
||||||
|
\cf0 Testers:\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/bdash"}}{\fldrslt Mark Rowe}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/danielctull"}}{\fldrslt Daniel Tull}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/davedelong"}}{\fldrslt Dave DeLong}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/esttorhe"}}{\fldrslt Esteban Torres}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/joeblau"}}{\fldrslt Joe Blau}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/marksands"}}{\fldrslt Mark Sands}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/mergesort"}}{\fldrslt Joe Fabisevich}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/phillco"}}{\fldrslt Phil Cohen}}\
|
||||||
|
{\field{\*\fldinst{HYPERLINK "https://github.com/zackdotcomputer"}}{\fldrslt Zack Sheppard}}}
|
||||||
@@ -24,8 +24,6 @@
|
|||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||||
<key>GitHubBuildLog</key>
|
|
||||||
<string>$(CI_BUILD_LINK)</string>
|
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>NSSupportsAutomaticTermination</key>
|
<key>NSSupportsAutomaticTermination</key>
|
||||||
|
|||||||
@@ -60,17 +60,16 @@ extension Preview {
|
|||||||
let id = UUID()
|
let id = UUID()
|
||||||
var name: String { "Modifiable Preview Store" }
|
var name: String { "Modifiable Preview Store" }
|
||||||
let secrets: [Secret]
|
let secrets: [Secret]
|
||||||
var supportedKeyTypes: KeyAvailability {
|
var supportedKeyTypes: [KeyType] {
|
||||||
return KeyAvailability(
|
if #available(macOS 26, *) {
|
||||||
available: [
|
[
|
||||||
.ecdsa256,
|
.ecdsa256,
|
||||||
.mldsa65,
|
.mldsa65,
|
||||||
.mldsa87
|
.mldsa87,
|
||||||
],
|
|
||||||
unavailable: [
|
|
||||||
.init(keyType: .ecdsa384, reason: .macOSUpdateRequired)
|
|
||||||
]
|
]
|
||||||
)
|
} else {
|
||||||
|
[.ecdsa256]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(secrets: [Secret]) {
|
init(secrets: [Secret]) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ struct ConfigurationItemView<Content: View>: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
switch action {
|
switch action {
|
||||||
case .copy(let string):
|
case .copy(let string):
|
||||||
Button(.copyableClickToCopyButton, systemImage: "doc.on.doc") {
|
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
|
||||||
NSPasteboard.general.declareTypes([.string], owner: nil)
|
NSPasteboard.general.declareTypes([.string], owner: nil)
|
||||||
NSPasteboard.general.setString(string, forType: .string)
|
NSPasteboard.general.setString(string, forType: .string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,19 +21,47 @@ struct IntegrationsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} detail: {
|
} detail: {
|
||||||
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
|
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
|
||||||
|
.fauxToolbar {
|
||||||
|
Button(.setupDoneButton) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.normalButton()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.toolbar {
|
|
||||||
Button(.setupDoneButton) {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hiddenToolbar()
|
|
||||||
.windowBackgroundStyle(.thinMaterial)
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
selectedInstruction = instructions.gettingStarted
|
selectedInstruction = instructions.gettingStarted
|
||||||
}
|
}
|
||||||
.frame(minWidth: 400, minHeight: 400)
|
.frame(minHeight: 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
|
||||||
|
modifier(FauxToolbarModifier(toolbarContent: content()))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FauxToolbarModifier<ToolbarContent: View>: ViewModifier {
|
||||||
|
|
||||||
|
var toolbarContent: ToolbarContent
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
content
|
||||||
|
Divider()
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
toolbarContent
|
||||||
|
.padding(.top, 8)
|
||||||
|
.padding(.trailing, 16)
|
||||||
|
.padding(.bottom, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,10 +85,7 @@ struct SetupView: View {
|
|||||||
integrations = true
|
integrations = true
|
||||||
}, content: {
|
}, content: {
|
||||||
IntegrationsView()
|
IntegrationsView()
|
||||||
.frame(minWidth: 500, minHeight: 400)
|
|
||||||
})
|
})
|
||||||
.frame(idealWidth: 600)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,13 +172,10 @@ struct StepView<Content: View>: View {
|
|||||||
.frame(width: 20)
|
.frame(width: 20)
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.bold()
|
.bold()
|
||||||
Text(description)
|
Text(description)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
if let detail {
|
if let detail {
|
||||||
Text(detail)
|
Text(detail)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.italic()
|
.italic()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ struct ToolConfigurationView: View {
|
|||||||
selectedSecret = created
|
selectedSecret = created
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fixedSize()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct WindowBackgroundStyleModifier: ViewModifier {
|
|
||||||
|
|
||||||
let shapeStyle: any ShapeStyle
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
if #available(macOS 15.0, *) {
|
|
||||||
content
|
|
||||||
.containerBackground(
|
|
||||||
shapeStyle, for: .window
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
|
|
||||||
func windowBackgroundStyle(_ style: some ShapeStyle) -> some View {
|
|
||||||
modifier(WindowBackgroundStyleModifier(shapeStyle: style))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HiddenToolbarModifier: ViewModifier {
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
if #available(macOS 15.0, *) {
|
|
||||||
content
|
|
||||||
.toolbarBackgroundVisibility(.hidden, for: .automatic)
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
|
|
||||||
func hiddenToolbar() -> some View {
|
|
||||||
modifier(HiddenToolbarModifier())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -75,23 +75,10 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
Section {
|
Section {
|
||||||
VStack {
|
VStack {
|
||||||
Picker(.createSecretKeyTypeLabel, selection: $keyType) {
|
Picker(.createSecretKeyTypeLabel, selection: $keyType) {
|
||||||
ForEach(store.supportedKeyTypes.available, id: \.self) { option in
|
ForEach(store.supportedKeyTypes, id: \.self) { option in
|
||||||
Text(String(describing: option))
|
Text(String(describing: option))
|
||||||
.tag(option)
|
.tag(option)
|
||||||
}
|
.font(.caption)
|
||||||
Divider()
|
|
||||||
ForEach(store.supportedKeyTypes.unavailable, id: \.keyType) { option in
|
|
||||||
VStack {
|
|
||||||
Button {
|
|
||||||
} label: {
|
|
||||||
Text(String(describing: option.keyType))
|
|
||||||
switch option.reason {
|
|
||||||
case .macOSUpdateRequired:
|
|
||||||
Text(.createSecretKeyTypeMacOSUpdateRequiredLabel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.selectionDisabled()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if keyType?.algorithm == .mldsa {
|
if keyType?.algorithm == .mldsa {
|
||||||
@@ -132,7 +119,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
keyType = store.supportedKeyTypes.available.first
|
keyType = store.supportedKeyTypes.first
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ extension View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ToolbarCircleButtonModifier: ViewModifier {
|
struct MenuButtonModifier: ViewModifier {
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
@@ -40,8 +40,8 @@ struct ToolbarCircleButtonModifier: ViewModifier {
|
|||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
|
||||||
func toolbarCircleButton() -> some View {
|
func menuButton() -> some View {
|
||||||
modifier(ToolbarCircleButtonModifier())
|
modifier(MenuButtonModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ToolbarStatusButtonStyle: ButtonStyle {
|
struct ToolbarButtonStyle: ButtonStyle {
|
||||||
|
|
||||||
private let lightColor: Color
|
private let lightColor: Color
|
||||||
private let darkColor: Color
|
private let darkColor: Color
|
||||||
@@ -39,7 +39,6 @@ struct ToolbarStatusButtonStyle: ButtonStyle {
|
|||||||
} else {
|
} else {
|
||||||
configuration
|
configuration
|
||||||
.label
|
.label
|
||||||
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
|
|
||||||
.background(colorScheme == .light ? lightColor : darkColor)
|
.background(colorScheme == .light ? lightColor : darkColor)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||||
@@ -56,24 +55,3 @@ struct ToolbarStatusButtonStyle: ButtonStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ToolbarButtonStyle: PrimitiveButtonStyle {
|
|
||||||
|
|
||||||
var tint: Color = .white.opacity(0.1)
|
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
|
||||||
if #available(macOS 26.0, *) {
|
|
||||||
configuration
|
|
||||||
.label
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.glassEffect(.regular.interactive().tint(tint))
|
|
||||||
} else {
|
|
||||||
BorderedButtonStyle().makeBody(configuration: configuration)
|
|
||||||
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct AboutView: View {
|
|
||||||
var body: some View {
|
|
||||||
if #available(macOS 15.0, *) {
|
|
||||||
AboutViewContent()
|
|
||||||
.containerBackground(
|
|
||||||
.thinMaterial, for: .window
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AboutViewContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AboutViewContent: View {
|
|
||||||
|
|
||||||
@Environment(\.openURL) var openURL
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 10) {
|
|
||||||
HStack {
|
|
||||||
Image(nsImage: NSApplication.shared.applicationIconImage)
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(verbatim: "Secretive")
|
|
||||||
.font(.system(.largeTitle, weight: .bold))
|
|
||||||
Text("**\(Bundle.main.versionNumber)** (\(Bundle.main.buildNumber))")
|
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
|
||||||
HStack {
|
|
||||||
Button(.aboutViewOnGithubButton) {
|
|
||||||
openURL(URL(string: "https://github.com/maxgoedjen/secretive")!)
|
|
||||||
}
|
|
||||||
.normalButton()
|
|
||||||
Button(.aboutBuildLogButton) {
|
|
||||||
openURL(Bundle.main.buildLog)
|
|
||||||
}
|
|
||||||
.normalButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(.aboutThanks(contributorsLink: "https://github.com/maxgoedjen/secretive/graphs/contributors", sponsorsLink: "https://github.com/sponsors/maxgoedjen"))
|
|
||||||
.font(.headline)
|
|
||||||
Text(.aboutOpenSourceNotice)
|
|
||||||
.font(.subheadline)
|
|
||||||
}
|
|
||||||
.padding(EdgeInsets(top: 10, leading: 30, bottom: 30, trailing: 30))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Bundle {
|
|
||||||
|
|
||||||
var buildLog: URL {
|
|
||||||
URL(string: infoDictionary!["GitHubBuildLog"] as! String)!
|
|
||||||
}
|
|
||||||
|
|
||||||
var versionNumber: String {
|
|
||||||
infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
var buildNumber: String {
|
|
||||||
infoDictionary?["CFBundleVersion"] as? String ?? "0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
AboutView()
|
|
||||||
.frame(width: 500, height: 250)
|
|
||||||
}
|
|
||||||
@@ -6,21 +6,19 @@ import Brief
|
|||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
|
@Binding var showingCreation: Bool
|
||||||
|
@Binding var runningSetup: Bool
|
||||||
|
@Binding var hasRunSetup: Bool
|
||||||
|
@State var showingAgentInfo = false
|
||||||
@State var activeSecret: AnySecret?
|
@State var activeSecret: AnySecret?
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
@Environment(\.secretStoreList) private var storeList
|
||||||
|
@Environment(\.updater) private var updater: any UpdaterProtocol
|
||||||
|
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||||
|
|
||||||
@State private var selectedUpdate: Release?
|
@State private var selectedUpdate: Release?
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
|
||||||
@Environment(\.openWindow) private var openWindow
|
|
||||||
@Environment(\.secretStoreList) private var storeList
|
|
||||||
@Environment(\.updater) private var updater
|
|
||||||
@Environment(\.agentStatusChecker) private var agentStatusChecker
|
|
||||||
|
|
||||||
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
|
|
||||||
@State private var showingCreation = false
|
|
||||||
@State private var showingAppPathNotice = false
|
@State private var showingAppPathNotice = false
|
||||||
@State private var runningSetup = false
|
|
||||||
@State private var showingAgentInfo = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
@@ -37,23 +35,6 @@ struct ContentView: View {
|
|||||||
toolbarItem(appPathNoticeView, id: "appPath")
|
toolbarItem(appPathNoticeView, id: "appPath")
|
||||||
toolbarItem(newItemView, id: "new")
|
toolbarItem(newItemView, id: "new")
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
if !hasRunSetup {
|
|
||||||
runningSetup = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.focusedSceneValue(\.showCreateSecret, .init(isEnabled: !runningSetup) {
|
|
||||||
showingCreation = true
|
|
||||||
})
|
|
||||||
.sheet(isPresented: $showingCreation) {
|
|
||||||
if let modifiable = storeList.modifiableStore {
|
|
||||||
CreateSecretView(store: modifiable) { created in
|
|
||||||
if let created {
|
|
||||||
activeSecret = created
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $runningSetup) {
|
.sheet(isPresented: $runningSetup) {
|
||||||
SetupView(setupComplete: $hasRunSetup)
|
SetupView(setupComplete: $hasRunSetup)
|
||||||
}
|
}
|
||||||
@@ -104,9 +85,24 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.buttonStyle(ToolbarStatusButtonStyle(color: color))
|
.buttonStyle(ToolbarButtonStyle(color: color))
|
||||||
.sheet(item: $selectedUpdate) { update in
|
.sheet(item: $selectedUpdate) { update in
|
||||||
UpdateDetailView(update: update)
|
VStack {
|
||||||
|
if updater.currentVersion.isTestBuild {
|
||||||
|
VStack {
|
||||||
|
if let description = updater.currentVersion.previewDescription {
|
||||||
|
Text(description)
|
||||||
|
}
|
||||||
|
Link(destination: URL(string: "https://github.com/maxgoedjen/secretive/actions/workflows/nightly.yml")!) {
|
||||||
|
Button(.updaterDownloadLatestNightlyButton) {}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.primaryButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
UpdateDetailView(update: update)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +113,16 @@ extension ContentView {
|
|||||||
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
||||||
showingCreation = true
|
showingCreation = true
|
||||||
}
|
}
|
||||||
.toolbarCircleButton()
|
.menuButton()
|
||||||
|
.sheet(isPresented: $showingCreation) {
|
||||||
|
if let modifiable = storeList.modifiableStore {
|
||||||
|
CreateSecretView(store: modifiable) { created in
|
||||||
|
if let created {
|
||||||
|
activeSecret = created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +149,7 @@ extension ContentView {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.buttonStyle(
|
.buttonStyle(
|
||||||
ToolbarStatusButtonStyle(
|
ToolbarButtonStyle(
|
||||||
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
||||||
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
||||||
)
|
)
|
||||||
@@ -166,18 +171,18 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.buttonStyle(ToolbarStatusButtonStyle(color: .orange))
|
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
||||||
.confirmationDialog(.appNotInApplicationsNoticeTitle, isPresented: $showingAppPathNotice) {
|
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
||||||
Button(.appNotInApplicationsNoticeCancelButton, role: .cancel) {
|
VStack {
|
||||||
|
Image(systemName: "exclamationmark.triangle")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 64)
|
||||||
|
Text(.appNotInApplicationsNoticeDetailDescription)
|
||||||
|
.frame(maxWidth: 300)
|
||||||
}
|
}
|
||||||
Button(.appNotInApplicationsNoticeQuitButton) {
|
.padding()
|
||||||
NSWorkspace.shared.selectFile(Bundle.main.bundlePath, inFileViewerRootedAtPath: Bundle.main.bundlePath)
|
|
||||||
NSApplication.shared.terminate(nil)
|
|
||||||
}
|
|
||||||
} message: {
|
|
||||||
Text(.appNotInApplicationsNoticeDetailDescription)
|
|
||||||
}
|
}
|
||||||
.dialogIcon(Image(systemName: "folder.fill.badge.questionmark"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ struct CopyableView: View {
|
|||||||
@State private var interactionState: InteractionState = .normal
|
@State private var interactionState: InteractionState = .normal
|
||||||
|
|
||||||
var content: some View {
|
var content: some View {
|
||||||
VStack(alignment: .leading, spacing: 15) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
image
|
image
|
||||||
.renderingMode(.template)
|
.renderingMode(.template)
|
||||||
@@ -28,19 +28,20 @@ struct CopyableView: View {
|
|||||||
}
|
}
|
||||||
copyButton
|
copyButton
|
||||||
}
|
}
|
||||||
.foregroundColor(secondaryTextColor)
|
.foregroundColor(secondaryTextColor)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20))
|
||||||
Divider()
|
Divider()
|
||||||
.ignoresSafeArea()
|
|
||||||
Text(text)
|
Text(text)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.foregroundColor(primaryTextColor)
|
.foregroundColor(primaryTextColor)
|
||||||
|
.padding(EdgeInsets(top: 10, leading: 20, bottom: 20, trailing: 20))
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.font(.system(.body, design: .monospaced))
|
.font(.system(.body, design: .monospaced))
|
||||||
}
|
}
|
||||||
.safeAreaPadding(20)
|
|
||||||
._background(interactionState: interactionState)
|
._background(interactionState: interactionState)
|
||||||
.frame(minWidth: 150, maxWidth: .infinity)
|
.frame(minWidth: 150, maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
@@ -52,12 +53,12 @@ struct CopyableView: View {
|
|||||||
interactionState = hovering ? .hovering : .normal
|
interactionState = hovering ? .hovering : .normal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.draggable(text) {
|
.onDrag({
|
||||||
content
|
NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: UTType.utf8PlainText.identifier)
|
||||||
.lineLimit(3)
|
}, preview: {
|
||||||
.frame(maxWidth: 300)
|
content
|
||||||
._background(interactionState: .dragging)
|
._background(interactionState: .dragging)
|
||||||
}
|
})
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
copy()
|
copy()
|
||||||
withAnimation {
|
withAnimation {
|
||||||
@@ -78,7 +79,7 @@ struct CopyableView: View {
|
|||||||
var copyButton: some View {
|
var copyButton: some View {
|
||||||
switch interactionState {
|
switch interactionState {
|
||||||
case .hovering:
|
case .hovering:
|
||||||
Button(.copyableClickToCopyButton, systemImage: "doc.on.doc") {
|
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
// Button will eat the click, so we set interaction state manually.
|
// Button will eat the click, so we set interaction state manually.
|
||||||
interactionState = .clicking
|
interactionState = .clicking
|
||||||
@@ -158,8 +159,7 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
|||||||
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
|
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
|
||||||
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
|
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
|
||||||
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15))
|
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15))
|
||||||
.mask(RoundedRectangle(cornerRadius: 15))
|
|
||||||
.shadow(color: .black.opacity(0.1), radius: 5)
|
|
||||||
} else {
|
} else {
|
||||||
content
|
content
|
||||||
.background(backgroundColor(interactionState: interactionState))
|
.background(backgroundColor(interactionState: interactionState))
|
||||||
@@ -170,25 +170,13 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
|||||||
|
|
||||||
func backgroundColor(interactionState: InteractionState) -> Color {
|
func backgroundColor(interactionState: InteractionState) -> Color {
|
||||||
guard appearsActive else { return Color.clear }
|
guard appearsActive else { return Color.clear }
|
||||||
if #available(macOS 26.0, *) {
|
switch interactionState {
|
||||||
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
|
case .normal:
|
||||||
switch interactionState {
|
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
|
||||||
case .normal:
|
case .hovering, .dragging:
|
||||||
return base
|
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
|
||||||
case .hovering:
|
case .clicking:
|
||||||
return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1)
|
return .accentColor
|
||||||
case .clicking, .dragging:
|
|
||||||
return base.mix(with: .accentColor, by: 0.8)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch interactionState {
|
|
||||||
case .normal:
|
|
||||||
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
|
|
||||||
case .hovering:
|
|
||||||
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
|
|
||||||
case .clicking, .dragging:
|
|
||||||
return .accentColor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,10 +184,7 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
VStack {
|
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
|
||||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
|
|
||||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
|
|
||||||
}
|
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,50 +3,61 @@ import Brief
|
|||||||
|
|
||||||
struct UpdateDetailView: View {
|
struct UpdateDetailView: View {
|
||||||
|
|
||||||
@Environment(\.updater) var updater
|
@Environment(\.updater) var updater: any UpdaterProtocol
|
||||||
@Environment(\.openURL) var openURL
|
|
||||||
|
|
||||||
let update: Release
|
let update: Release
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack {
|
||||||
HStack {
|
Text(.updateVersionName(updateName: update.name)).font(.title)
|
||||||
if !update.critical {
|
GroupBox(label: Text(.updateReleaseNotesTitle)) {
|
||||||
Button(.updateIgnoreButton) {
|
ScrollView {
|
||||||
Task {
|
attributedBody
|
||||||
await updater.ignore(release: update)
|
}
|
||||||
}
|
}
|
||||||
|
HStack {
|
||||||
|
if !update.critical {
|
||||||
|
Button(.updateIgnoreButton) {
|
||||||
|
Task {
|
||||||
|
await updater.ignore(release: update)
|
||||||
}
|
}
|
||||||
.buttonStyle(ToolbarButtonStyle())
|
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
if updater.currentVersion.isTestBuild {
|
|
||||||
Button(.updaterDownloadLatestNightlyButton) {
|
|
||||||
openURL(URL(string: "https://github.com/maxgoedjen/secretive/actions/workflows/nightly.yml")!)
|
|
||||||
}
|
|
||||||
.buttonStyle(ToolbarButtonStyle(tint: .accentColor))
|
|
||||||
}
|
|
||||||
Button(.updateUpdateButton) {
|
|
||||||
openURL(update.html_url)
|
|
||||||
}
|
|
||||||
.buttonStyle(ToolbarButtonStyle(tint: .accentColor))
|
|
||||||
.keyboardShortcut(.defaultAction)
|
|
||||||
}
|
}
|
||||||
.padding()
|
Button(.updateUpdateButton) {
|
||||||
Divider()
|
NSWorkspace.shared.open(update.html_url)
|
||||||
Form {
|
|
||||||
Section {
|
|
||||||
Text(update.attributedBody)
|
|
||||||
} header: {
|
|
||||||
Text(.updateVersionName(updateName: update.name)) .headerProminence(.increased)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.keyboardShortcut(.defaultAction)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
var attributedBody: Text {
|
||||||
|
var text = Text(verbatim: "")
|
||||||
|
for line in update.body.split(whereSeparator: \.isNewline) {
|
||||||
|
let attributed: Text
|
||||||
|
let split = line.split(separator: " ")
|
||||||
|
let unprefixed = split.dropFirst().joined(separator: " ")
|
||||||
|
if let prefix = split.first {
|
||||||
|
switch prefix {
|
||||||
|
case "#":
|
||||||
|
attributed = Text(unprefixed).font(.title) + Text(verbatim: "\n")
|
||||||
|
case "##":
|
||||||
|
attributed = Text(unprefixed).font(.title2) + Text(verbatim: "\n")
|
||||||
|
case "###":
|
||||||
|
attributed = Text(unprefixed).font(.title3) + Text(verbatim: "\n")
|
||||||
|
default:
|
||||||
|
attributed = Text(line) + Text(verbatim: "\n\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attributed = Text(line) + Text(verbatim: "\n\n")
|
||||||
|
}
|
||||||
|
text = text + attributed
|
||||||
|
}
|
||||||
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
|
||||||
UpdateDetailView(update: .init(name: "3.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Hello"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ final class SecretiveUpdater: NSObject, XPCProtocol {
|
|||||||
|
|
||||||
func process(_: Data) async throws -> [Release] {
|
func process(_: Data) async throws -> [Release] {
|
||||||
let (data, _) = try await URLSession.shared.data(from: Constants.updateURL)
|
let (data, _) = try await URLSession.shared.data(from: Constants.updateURL)
|
||||||
return try JSONDecoder()
|
return try JSONDecoder().decode([Release].self, from: data)
|
||||||
.decode([GitHubRelease].self, from: data)
|
|
||||||
.map(Release.init)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||