Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f74bd814f | ||
|
|
d9d93574f2 | ||
|
|
15e8ed1ec2 | ||
|
|
1df0c8e96b | ||
|
|
8213a8b451 | ||
|
|
af77fd4a21 | ||
|
|
85d0cab0f5 | ||
|
|
e8cdcdfb7f | ||
|
|
d7f8d5e56b | ||
|
|
3f247d628f | ||
|
|
dae9cead4e | ||
|
|
fe9f8613fa | ||
|
|
5d5ae5bab4 | ||
|
|
f76766a9d5 | ||
|
|
b308b10716 | ||
|
|
0e1e6813a1 | ||
|
|
27bf7c29e4 | ||
|
|
36b6c52979 | ||
|
|
67ec4fee12 | ||
|
|
21fc834fd9 | ||
|
|
726d0580d0 | ||
|
|
4f608ebbc6 | ||
|
|
6e7cf82618 |
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/Secretive/Credits.rtf
|
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||||
- 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: ${{ steps.upload.outputs.artifact-digest }}
|
subject-digest: sha256:${{ 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 Secretive.zip
|
gh release upload $TAG_NAME Secretive.zip
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAG_NAME: ${{ github.ref }}
|
TAG_NAME: ${{ github.ref }}
|
||||||
|
|||||||
11
README.md
@@ -1,8 +1,7 @@
|
|||||||
# 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 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.
|
Secretive is an app for protecting and managing SSH keys with the Secure Enclave.
|
||||||
|
|
||||||
<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">
|
||||||
@@ -62,3 +61,11 @@ 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,2 +1,3 @@
|
|||||||
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,6 +177,73 @@
|
|||||||
"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" : {
|
||||||
@@ -316,8 +383,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"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."
|
"value" : "Secretive не получлось запустить SecretAgent. Пожалуйста, попробуйте перезапустить Ваш Mac, и если это не поможет – создайте issue на GitHub."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -501,8 +568,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Disable Agent"
|
"value" : "Отключить агент"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -686,8 +753,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Restart Agent"
|
"value" : "Перезапустить агент"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -871,8 +938,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Running Since"
|
"value" : "Запущено с"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -1056,8 +1123,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Socket Path"
|
"value" : "Путь к сокету"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -1241,8 +1308,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Start Agent"
|
"value" : "Запустить агент"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -1426,8 +1493,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Starting Agent"
|
"value" : "Агент запускается"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -1611,8 +1678,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Version"
|
"value" : "Версия"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -1796,8 +1863,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"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.**"
|
"value" : "SecretAgent это процесс, который работает в фоне чтобы подписывать запросы. Так Вам не придется все время держать Secretive открытым.\n\n**Secretive не сможет нормально функционировать, пока агент не установлен и не запущен.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -2721,8 +2788,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Secret Agent Location"
|
"value" : "Расположение Secret Agent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -3139,6 +3206,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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" : {
|
||||||
@@ -3324,6 +3408,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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" : {
|
||||||
@@ -4574,8 +4675,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Advanced"
|
"value" : "Расширенное"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -4759,8 +4860,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "If you change your biometric settings in _any way_, including adding a new fingerprint, this key will no longer be accessible."
|
"value" : "Если Вы сделаете _любые_ изменения в Ваших настройках биометрии, включая добавление нового отпечатка пальца, этот ключ перестанет быть доступным."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -5314,8 +5415,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "This shows at the end of your public key. It’s usually an email address."
|
"value" : "Это отображается в конце Вашего публичного ключа. Как правило, это email адрес."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -5499,8 +5600,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Key Attribution"
|
"value" : "Принадлежность ключа"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -5684,8 +5785,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Key Type"
|
"value" : "Тип ключа"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -5732,6 +5833,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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" : {
|
||||||
@@ -5869,8 +5981,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"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."
|
"value" : "Внимание: Ключи ML-DSA совсем новые, и не пока поддерживаются многими серверами. Пожалуйста удостоверьтесь, что используемый с этим ключом сервер поддерживает ключи ML-DSA."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -6425,7 +6537,7 @@
|
|||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Аутентификация не потребуется пока Ваш Mac разблокирован, но Вы получите уведомление, если секрет был использован."
|
"value" : "Аутентификация не потребуется пока Ваш Mac разблокирован, но Вы получите уведомление, если секрет будет использован."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -6794,8 +6906,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Protection Level"
|
"value" : "Уровень защиты"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -6979,8 +7091,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Require authentication with current set of biometrics."
|
"value" : "Требовать аутентификацию с текущим набором биометрии."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -7164,8 +7276,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Current Biometrics"
|
"value" : "Текущая биометрия"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -9384,8 +9496,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"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."
|
"value" : "Похоже, что Вы могли недавно обновить MacOS. Иногда из-за этого возникают проблемы с Secure Enclave, и возможно Вам потребуется перезапустить Ваш Mac, чтобы их исправить."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -9569,8 +9681,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Missing Secrets?"
|
"value" : "Не хватает секретов?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -10667,8 +10779,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Add This:"
|
"value" : "Добавить следующее:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -10852,8 +10964,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Apps"
|
"value" : "Приложения"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11037,8 +11149,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"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."
|
"value" : "Существует список инструкций, поддерживаемый сообществом, к приложениям на GitHub. Если нужное Вам приложение не поддерживается, создайте issue, возможно сообщество сможет помочь Вам."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11222,8 +11334,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"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."
|
"value" : "Существует список shell-скриптов, поддерживаемый сообществом на GitHub. Если нужный Вам shell-скрипт не поддерживается, создайте issue, возможно сообщество сможет помочь Вам."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11407,8 +11519,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "You'll need to create a Secret before configuring this action."
|
"value" : "Вам потребуется создать секрет перед настройкой этого действия."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11592,8 +11704,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Configure Using Secret"
|
"value" : "Настроить используя секрет"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11777,8 +11889,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "No Secret"
|
"value" : "Без секрета"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -11962,8 +12074,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Secret"
|
"value" : "С секретом"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12147,8 +12259,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "You can configure more than one tool, they generally won't interfere with each other."
|
"value" : "Вы можете настроить более чем один инструмент. Обычно это не вызывает конфликтов при работе."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12332,8 +12444,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Getting Started"
|
"value" : "Начало работы"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12517,8 +12629,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Integrations"
|
"value" : "Интеграции"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12702,8 +12814,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "If you're trying to sign your git commits, set up Git Signing."
|
"value" : "Если Вы хотите подписывать ваши git коммиты, настройте подпись Git."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -12887,8 +12999,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "If you're trying to configure anything your command line runs to use Secretive, configure your shell."
|
"value" : "Если Вы хотите наладить работу Secretive с Вашими скриптами в Терминале, настройте Ваш Терминал."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13072,8 +13184,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "If you don't known what shell you use and haven't changed it, you're probably using `%(shellName)@`."
|
"value" : "Если Вы не знаете, какой Терминал используете и не меняли его раннее, то скорее всего Вы используете `%(shellName)@`."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13257,8 +13369,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client."
|
"value" : "Если Вы хотите аутентифицироваться с SSH сервером, или с сервисом как GitHub через SSH, настройте Ваш SSH клиент."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13442,8 +13554,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Configuring Tools for Secretive"
|
"value" : "Настройка инструментов для Secretive"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13627,8 +13739,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"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."
|
"value" : "Большинство инструментов попробуют найти SSH ключи в `~/.ssh`. Для того, чтобы использовать Secretive, нам потребуется настроить эти инструменты, чтобы они работали с Secretive."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13812,8 +13924,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "What Should I Configure?"
|
"value" : "Что мне следует настроить?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -13997,8 +14109,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "~/.gitallowedsigners probably does not exist. You'll need to create it."
|
"value" : "Возможно, директория ~/.gitallowedsigners не существует. Вам потребуется создать ее."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -14368,8 +14480,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Integrations…"
|
"value" : "Интеграции…"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -14553,8 +14665,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Other"
|
"value" : "Другое"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -14738,8 +14850,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "other"
|
"value" : "другое"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -14923,8 +15035,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Configuration File"
|
"value" : "Файл конфигурации"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -15108,7 +15220,7 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Shell"
|
"value" : "Shell"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -15293,8 +15405,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "You can tell SSH to use a specific key for a given host. See the web documentation for more details."
|
"value" : "Вы можете настроить SSH так, чтобы использовался конкретный ключ для выбранного хоста. Посмотрите документацию в интернете, чтобы узнать подробности."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -15478,8 +15590,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "System"
|
"value" : "Система"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -16035,8 +16147,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Git Signing"
|
"value" : "Подпись Git"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -16592,8 +16704,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "View on GitHub"
|
"value" : "Посмотреть на GitHub"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -16777,8 +16889,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "View Documentation on Web"
|
"value" : "Найти документацию в интернете"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -16962,8 +17074,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "If any section (like [user]) already exists, just add the entries in the existing section."
|
"value" : "Если любой раздел (как [user]) уже существует, просто добавьте новые записи в него."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -17890,7 +18002,7 @@
|
|||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Не разблокировывать"
|
"value" : "Не разблокировать"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -18074,8 +18186,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Reveal in Finder"
|
"value" : "Открыть в Finder"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -19369,7 +19481,7 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Secure Enclave"
|
"value" : "Secure Enclave"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -19734,7 +19846,7 @@
|
|||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Это вспомогательное приложение назвается **Secret Agent**, Вы можете наблюдать его в Activity Monitor время от времени."
|
"value" : "Это вспомогательное приложение назвается **Secret Agent**, Вы можете видеть его в Activity Monitor время от времени."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -20289,7 +20401,7 @@
|
|||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Настроить агента"
|
"value" : "Настроить агент"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -20473,8 +20585,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Done"
|
"value" : "Готово"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -20658,8 +20770,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Configure"
|
"value" : "Настроить"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -20843,8 +20955,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Tell the tools you use how to talk to Secretive."
|
"value" : "Настройте Ваши инструменты для работы с Secretive"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -21028,8 +21140,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Configure Integrations"
|
"value" : "Настроить интеграции"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -21768,8 +21880,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Done"
|
"value" : "Готово"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
@@ -24735,7 +24847,7 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Secretive %1$(updateName)@"
|
"value" : "Secretive %1$(updateName)@"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -24920,8 +25032,8 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Download Latest Nightly Build"
|
"value" : "Скачать последнюю ночную сборку"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr" : {
|
"sr" : {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
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 {
|
public struct Release: Codable, Sendable, Hashable {
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -15,6 +16,8 @@ public struct Release: Codable, Sendable {
|
|||||||
/// 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.
|
||||||
@@ -26,6 +29,56 @@ public struct Release: Codable, Sendable {
|
|||||||
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 () -> [KeyType]
|
private let _supportedKeyTypes: @Sendable () -> KeyAvailability
|
||||||
|
|
||||||
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: [KeyType] {
|
public var supportedKeyTypes: KeyAvailability {
|
||||||
_supportedKeyTypes()
|
_supportedKeyTypes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ 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) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
let validPaths = Set(secrets.map { publicKeyPath(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)/\($0)" }
|
let fullPathContents = contentsOfDirectory.map { directory.appending(path: $0).path() }
|
||||||
|
|
||||||
let untracked = Set(fullPathContents)
|
let untracked = Set(fullPathContents)
|
||||||
.subtracting(validPaths)
|
.subtracting(validPaths)
|
||||||
|
|||||||
@@ -62,10 +62,37 @@ 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: [KeyType] { get }
|
var supportedKeyTypes: KeyAvailability { 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.")
|
||||||
try markMigrated(secret: secret, oldID: id)
|
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).")
|
||||||
try markMigrated(secret: secret, oldID: id)
|
markMigrated(secret: secret, oldID: id)
|
||||||
migratedAny = true
|
migratedAny = true
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to migrate \(name): \(error).")
|
logger.error("Failed to migrate \(name): \(error.localizedDescription).")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if migratedAny {
|
if migratedAny {
|
||||||
@@ -69,10 +69,10 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
public func markMigrated(secret: Secret, oldID: Data) throws {
|
public func markMigrated(secret: Secret, oldID: Data) {
|
||||||
let updateQuery = KeychainDictionary([
|
let updateQuery = KeychainDictionary([
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrApplicationLabel: secret.id
|
kSecAttrApplicationLabel: oldID
|
||||||
])
|
])
|
||||||
|
|
||||||
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 {
|
||||||
throw KeychainError(statusCode: status)
|
logger.warning("Failed to mark \(secret.name) as migrated: \(status).")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,17 +186,22 @@ extension SecureEnclave {
|
|||||||
await reloadSecrets()
|
await reloadSecrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var supportedKeyTypes: [KeyType] {
|
public let supportedKeyTypes: KeyAvailability = {
|
||||||
if #available(macOS 26, *) {
|
let macOS26Keys: [KeyType] = [.mldsa65, .mldsa87]
|
||||||
[
|
let isAtLeastMacOS26 = if #available(macOS 26, *) {
|
||||||
.ecdsa256,
|
true
|
||||||
.mldsa65,
|
|
||||||
.mldsa87,
|
|
||||||
]
|
|
||||||
} else {
|
} else {
|
||||||
[.ecdsa256]
|
false
|
||||||
}
|
}
|
||||||
|
return KeyAvailability(
|
||||||
|
available: [
|
||||||
|
.ecdsa256,
|
||||||
|
] + (isAtLeastMacOS26 ? macOS26Keys : []),
|
||||||
|
unavailable: (isAtLeastMacOS26 ? [] : macOS26Keys).map {
|
||||||
|
KeyAvailability.UnavailableKeyType(keyType: $0, reason: .macOSUpdateRequired)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ 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 {
|
||||||
reply(nil, error)
|
// Sending cast directly tries to serialize it and crashes XPCEncoder.
|
||||||
|
let cast = error as NSError
|
||||||
|
reply(nil, NSError(domain: cast.domain, code: cast.code, userInfo: [NSLocalizedDescriptionKey: error.localizedDescription]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,24 @@ 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 {
|
||||||
try await session.send(data)
|
logger.debug("Parsing input")
|
||||||
|
defer { logger.debug("Parsed input") }
|
||||||
|
return try await session.send(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
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, ); }; };
|
||||||
@@ -36,13 +35,11 @@
|
|||||||
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 */; };
|
||||||
@@ -73,6 +70,10 @@
|
|||||||
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 */
|
||||||
@@ -197,7 +198,6 @@
|
|||||||
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,7 +205,6 @@
|
|||||||
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; };
|
||||||
@@ -239,6 +238,9 @@
|
|||||||
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 */
|
||||||
@@ -293,15 +295,16 @@
|
|||||||
path = Helpers;
|
path = Helpers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
504788ED2E681EB200B4556F /* Styles */ = {
|
504788ED2E681EB200B4556F /* Modifiers */ = {
|
||||||
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 = Styles;
|
path = Modifiers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
504788EE2E681EC300B4556F /* Secrets */ = {
|
504788EE2E681EC300B4556F /* Secrets */ = {
|
||||||
@@ -335,6 +338,7 @@
|
|||||||
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 */,
|
||||||
@@ -375,11 +379,10 @@
|
|||||||
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 */,
|
||||||
);
|
);
|
||||||
@@ -432,7 +435,7 @@
|
|||||||
children = (
|
children = (
|
||||||
504788EF2E681ED700B4556F /* Configuration */,
|
504788EF2E681ED700B4556F /* Configuration */,
|
||||||
504788EE2E681EC300B4556F /* Secrets */,
|
504788EE2E681EC300B4556F /* Secrets */,
|
||||||
504788ED2E681EB200B4556F /* Styles */,
|
504788ED2E681EB200B4556F /* Modifiers */,
|
||||||
504788F02E681F0100B4556F /* Views */,
|
504788F02E681F0100B4556F /* Views */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
@@ -643,9 +646,8 @@
|
|||||||
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;
|
||||||
@@ -671,8 +673,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;
|
||||||
};
|
};
|
||||||
@@ -685,7 +687,9 @@
|
|||||||
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 */,
|
||||||
@@ -1045,7 +1049,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 = 26.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
@@ -1085,7 +1089,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 = 26.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
@@ -1127,7 +1131,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 = 26.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
@@ -1159,7 +1163,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 = 26.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
@@ -1189,7 +1193,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 = 26.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.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)";
|
||||||
@@ -1221,7 +1225,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 = 26.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.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,55 +4,18 @@ 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(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
ContentView()
|
||||||
.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 {
|
||||||
@@ -62,25 +25,52 @@ 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") {
|
||||||
showingIntegrations = true
|
openWindow(id: String(describing: IntegrationsView.self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CommandGroup(after: CommandGroupPlacement.newItem) {
|
CommandGroup(after: CommandGroupPlacement.newItem) {
|
||||||
Button(.appMenuNewSecretButton) {
|
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
||||||
showingCreation = true
|
showCreateSecret?()
|
||||||
}
|
}
|
||||||
.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) {
|
||||||
NSWorkspace.shared.open(Constants.helpURL)
|
openURL(Constants.helpURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SidebarCommands()
|
SidebarCommands()
|
||||||
@@ -113,8 +103,56 @@ 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
|
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 |
@@ -1,68 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 856 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 356 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 356 KiB |
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{\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,6 +24,8 @@
|
|||||||
<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,16 +60,17 @@ 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: [KeyType] {
|
var supportedKeyTypes: KeyAvailability {
|
||||||
if #available(macOS 26, *) {
|
return KeyAvailability(
|
||||||
[
|
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: "document.on.document") {
|
Button(.copyableClickToCopyButton, systemImage: "doc.on.doc") {
|
||||||
NSPasteboard.general.declareTypes([.string], owner: nil)
|
NSPasteboard.general.declareTypes([.string], owner: nil)
|
||||||
NSPasteboard.general.setString(string, forType: .string)
|
NSPasteboard.general.setString(string, forType: .string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,46 +22,18 @@ struct IntegrationsView: View {
|
|||||||
}
|
}
|
||||||
} detail: {
|
} detail: {
|
||||||
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
|
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
|
||||||
.fauxToolbar {
|
}
|
||||||
|
.toolbar {
|
||||||
Button(.setupDoneButton) {
|
Button(.setupDoneButton) {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.normalButton()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.hiddenToolbar()
|
||||||
|
.windowBackgroundStyle(.thinMaterial)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
selectedInstruction = instructions.gettingStarted
|
selectedInstruction = instructions.gettingStarted
|
||||||
}
|
}
|
||||||
.frame(minHeight: 500)
|
.frame(minWidth: 400, minHeight: 400)
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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,7 +85,10 @@ struct SetupView: View {
|
|||||||
integrations = true
|
integrations = true
|
||||||
}, content: {
|
}, content: {
|
||||||
IntegrationsView()
|
IntegrationsView()
|
||||||
|
.frame(minWidth: 500, minHeight: 400)
|
||||||
})
|
})
|
||||||
|
.frame(idealWidth: 600)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,10 +175,13 @@ 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,6 +32,7 @@ struct ToolConfigurationView: View {
|
|||||||
selectedSecret = created
|
selectedSecret = created
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.fixedSize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ extension View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MenuButtonModifier: ViewModifier {
|
struct ToolbarCircleButtonModifier: 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 MenuButtonModifier: ViewModifier {
|
|||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
|
||||||
func menuButton() -> some View {
|
func toolbarCircleButton() -> some View {
|
||||||
modifier(MenuButtonModifier())
|
modifier(ToolbarCircleButtonModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ToolbarButtonStyle: ButtonStyle {
|
struct ToolbarStatusButtonStyle: ButtonStyle {
|
||||||
|
|
||||||
private let lightColor: Color
|
private let lightColor: Color
|
||||||
private let darkColor: Color
|
private let darkColor: Color
|
||||||
@@ -39,6 +39,7 @@ struct ToolbarButtonStyle: 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))
|
||||||
@@ -55,3 +56,24 @@ struct ToolbarButtonStyle: 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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,10 +75,23 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
Section {
|
Section {
|
||||||
VStack {
|
VStack {
|
||||||
Picker(.createSecretKeyTypeLabel, selection: $keyType) {
|
Picker(.createSecretKeyTypeLabel, selection: $keyType) {
|
||||||
ForEach(store.supportedKeyTypes, id: \.self) { option in
|
ForEach(store.supportedKeyTypes.available, 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 {
|
||||||
@@ -119,7 +132,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
keyType = store.supportedKeyTypes.first
|
keyType = store.supportedKeyTypes.available.first
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
}
|
}
|
||||||
|
|||||||
69
Sources/Secretive/Views/Views/AboutView.swift
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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,19 +6,21 @@ 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 {
|
||||||
@@ -35,6 +37,23 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -85,27 +104,12 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.buttonStyle(ToolbarButtonStyle(color: color))
|
.buttonStyle(ToolbarStatusButtonStyle(color: color))
|
||||||
.sheet(item: $selectedUpdate) { update in
|
.sheet(item: $selectedUpdate) { update in
|
||||||
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)
|
UpdateDetailView(update: update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var newItemView: some View {
|
var newItemView: some View {
|
||||||
@@ -113,16 +117,7 @@ extension ContentView {
|
|||||||
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
||||||
showingCreation = true
|
showingCreation = true
|
||||||
}
|
}
|
||||||
.menuButton()
|
.toolbarCircleButton()
|
||||||
.sheet(isPresented: $showingCreation) {
|
|
||||||
if let modifiable = storeList.modifiableStore {
|
|
||||||
CreateSecretView(store: modifiable) { created in
|
|
||||||
if let created {
|
|
||||||
activeSecret = created
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +144,7 @@ extension ContentView {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.buttonStyle(
|
.buttonStyle(
|
||||||
ToolbarButtonStyle(
|
ToolbarStatusButtonStyle(
|
||||||
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),
|
||||||
)
|
)
|
||||||
@@ -171,18 +166,18 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
.buttonStyle(ToolbarStatusButtonStyle(color: .orange))
|
||||||
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
.confirmationDialog(.appNotInApplicationsNoticeTitle, isPresented: $showingAppPathNotice) {
|
||||||
VStack {
|
Button(.appNotInApplicationsNoticeCancelButton, role: .cancel) {
|
||||||
Image(systemName: "exclamationmark.triangle")
|
}
|
||||||
.resizable()
|
Button(.appNotInApplicationsNoticeQuitButton) {
|
||||||
.aspectRatio(contentMode: .fit)
|
NSWorkspace.shared.selectFile(Bundle.main.bundlePath, inFileViewerRootedAtPath: Bundle.main.bundlePath)
|
||||||
.frame(width: 64)
|
NSApplication.shared.terminate(nil)
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
Text(.appNotInApplicationsNoticeDetailDescription)
|
Text(.appNotInApplicationsNoticeDetailDescription)
|
||||||
.frame(maxWidth: 300)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
|
.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) {
|
VStack(alignment: .leading, spacing: 15) {
|
||||||
HStack {
|
HStack {
|
||||||
image
|
image
|
||||||
.renderingMode(.template)
|
.renderingMode(.template)
|
||||||
@@ -31,17 +31,16 @@ struct CopyableView: View {
|
|||||||
.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)
|
||||||
}
|
}
|
||||||
@@ -53,12 +52,12 @@ struct CopyableView: View {
|
|||||||
interactionState = hovering ? .hovering : .normal
|
interactionState = hovering ? .hovering : .normal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDrag({
|
.draggable(text) {
|
||||||
NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: UTType.utf8PlainText.identifier)
|
|
||||||
}, preview: {
|
|
||||||
content
|
content
|
||||||
|
.lineLimit(3)
|
||||||
|
.frame(maxWidth: 300)
|
||||||
._background(interactionState: .dragging)
|
._background(interactionState: .dragging)
|
||||||
})
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
copy()
|
copy()
|
||||||
withAnimation {
|
withAnimation {
|
||||||
@@ -79,7 +78,7 @@ struct CopyableView: View {
|
|||||||
var copyButton: some View {
|
var copyButton: some View {
|
||||||
switch interactionState {
|
switch interactionState {
|
||||||
case .hovering:
|
case .hovering:
|
||||||
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
|
Button(.copyableClickToCopyButton, systemImage: "doc.on.doc") {
|
||||||
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
|
||||||
@@ -159,7 +158,8 @@ 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,21 +170,36 @@ 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, *) {
|
||||||
|
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
|
||||||
|
switch interactionState {
|
||||||
|
case .normal:
|
||||||
|
return base
|
||||||
|
case .hovering:
|
||||||
|
return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1)
|
||||||
|
case .clicking, .dragging:
|
||||||
|
return base.mix(with: .accentColor, by: 0.8)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
switch interactionState {
|
switch interactionState {
|
||||||
case .normal:
|
case .normal:
|
||||||
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
|
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
|
||||||
case .hovering, .dragging:
|
case .hovering:
|
||||||
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
|
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
|
||||||
case .clicking:
|
case .clicking, .dragging:
|
||||||
return .accentColor
|
return .accentColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#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,18 +3,13 @@ import Brief
|
|||||||
|
|
||||||
struct UpdateDetailView: View {
|
struct UpdateDetailView: View {
|
||||||
|
|
||||||
@Environment(\.updater) var updater: any UpdaterProtocol
|
@Environment(\.updater) var updater
|
||||||
|
@Environment(\.openURL) var openURL
|
||||||
|
|
||||||
let update: Release
|
let update: Release
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack(spacing: 0) {
|
||||||
Text(.updateVersionName(updateName: update.name)).font(.title)
|
|
||||||
GroupBox(label: Text(.updateReleaseNotesTitle)) {
|
|
||||||
ScrollView {
|
|
||||||
attributedBody
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HStack {
|
HStack {
|
||||||
if !update.critical {
|
if !update.critical {
|
||||||
Button(.updateIgnoreButton) {
|
Button(.updateIgnoreButton) {
|
||||||
@@ -22,42 +17,36 @@ struct UpdateDetailView: View {
|
|||||||
await updater.ignore(release: update)
|
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) {
|
Button(.updateUpdateButton) {
|
||||||
NSWorkspace.shared.open(update.html_url)
|
openURL(update.html_url)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(ToolbarButtonStyle(tint: .accentColor))
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
.padding()
|
.padding()
|
||||||
.frame(maxWidth: 500)
|
Divider()
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
Text(update.attributedBody)
|
||||||
|
} header: {
|
||||||
|
Text(.updateVersionName(updateName: update.name)) .headerProminence(.increased)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
.formStyle(.grouped)
|
||||||
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,7 +11,9 @@ 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().decode([Release].self, from: data)
|
return try JSONDecoder()
|
||||||
|
.decode([GitHubRelease].self, from: data)
|
||||||
|
.map(Release.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||