Dadoum commited on 2026-07-08 09:55:10
Showing 31 changed files, with 8797 additions and 1684 deletions.
| ... | ... |
@@ -577,6 +577,8 @@ dependencies = [ |
| 577 | 577 |
"futures-lite 2.6.1", |
| 578 | 578 |
"pin-project", |
| 579 | 579 |
"thiserror 2.0.18", |
| 580 |
+ "tokio", |
|
| 581 |
+ "tokio-util", |
|
| 580 | 582 |
] |
| 581 | 583 |
|
| 582 | 584 |
[[package]] |
| ... | ... |
@@ -1781,6 +1783,15 @@ dependencies = [ |
| 1781 | 1783 |
"ctutils", |
| 1782 | 1784 |
] |
| 1783 | 1785 |
|
| 1786 |
+[[package]] |
|
| 1787 |
+name = "directories" |
|
| 1788 |
+version = "6.0.0" |
|
| 1789 |
+source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1790 |
+checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" |
|
| 1791 |
+dependencies = [ |
|
| 1792 |
+ "dirs-sys", |
|
| 1793 |
+] |
|
| 1794 |
+ |
|
| 1784 | 1795 |
[[package]] |
| 1785 | 1796 |
name = "dirs" |
| 1786 | 1797 |
version = "6.0.0" |
| ... | ... |
@@ -3338,6 +3349,26 @@ dependencies = [ |
| 3338 | 3349 |
"zerovec", |
| 3339 | 3350 |
] |
| 3340 | 3351 |
|
| 3352 |
+[[package]] |
|
| 3353 |
+name = "idevice" |
|
| 3354 |
+version = "0.1.64" |
|
| 3355 |
+source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 3356 |
+checksum = "48e95040e91503e7425835843e5e985ab1ea440db9b965972021e7b845c2a6ae" |
|
| 3357 |
+dependencies = [ |
|
| 3358 |
+ "base64", |
|
| 3359 |
+ "futures", |
|
| 3360 |
+ "libc", |
|
| 3361 |
+ "plist", |
|
| 3362 |
+ "plist-macro", |
|
| 3363 |
+ "rand 0.10.2", |
|
| 3364 |
+ "rustls", |
|
| 3365 |
+ "serde", |
|
| 3366 |
+ "thiserror 2.0.18", |
|
| 3367 |
+ "tokio", |
|
| 3368 |
+ "tokio-rustls", |
|
| 3369 |
+ "tracing", |
|
| 3370 |
+] |
|
| 3371 |
+ |
|
| 3341 | 3372 |
[[package]] |
| 3342 | 3373 |
name = "idna" |
| 3343 | 3374 |
version = "1.1.0" |
| ... | ... |
@@ -4870,6 +4901,15 @@ dependencies = [ |
| 4870 | 4901 |
"time", |
| 4871 | 4902 |
] |
| 4872 | 4903 |
|
| 4904 |
+[[package]] |
|
| 4905 |
+name = "plist-macro" |
|
| 4906 |
+version = "0.1.6" |
|
| 4907 |
+source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 4908 |
+checksum = "82c39e020f2d8d361e91a434cfe26bc7c3e21ae54414379816d0110f3f7ca121" |
|
| 4909 |
+dependencies = [ |
|
| 4910 |
+ "plist", |
|
| 4911 |
+] |
|
| 4912 |
+ |
|
| 4873 | 4913 |
[[package]] |
| 4874 | 4914 |
name = "plist-macros" |
| 4875 | 4915 |
version = "0.1.0" |
| ... | ... |
@@ -5518,7 +5558,9 @@ dependencies = [ |
| 5518 | 5558 |
"base64", |
| 5519 | 5559 |
"bytes", |
| 5520 | 5560 |
"encoding_rs", |
| 5561 |
+ "futures-channel", |
|
| 5521 | 5562 |
"futures-core", |
| 5563 |
+ "futures-util", |
|
| 5522 | 5564 |
"h2", |
| 5523 | 5565 |
"http", |
| 5524 | 5566 |
"http-body", |
| ... | ... |
@@ -6477,9 +6519,31 @@ dependencies = [ |
| 6477 | 6519 |
name = "super-sideloader" |
| 6478 | 6520 |
version = "0.1.0" |
| 6479 | 6521 |
dependencies = [ |
| 6522 |
+ "adi", |
|
| 6523 |
+ "adid-proxy", |
|
| 6524 |
+ "android-coreadi", |
|
| 6525 |
+ "async_zip", |
|
| 6526 |
+ "core-foundation 0.10.0", |
|
| 6527 |
+ "directories", |
|
| 6528 |
+ "dlopen2", |
|
| 6480 | 6529 |
"env_logger", |
| 6530 |
+ "futures", |
|
| 6531 |
+ "futures-lite 2.6.1", |
|
| 6481 | 6532 |
"gpui", |
| 6482 | 6533 |
"gpui_platform", |
| 6534 |
+ "grandslam", |
|
| 6535 |
+ "idevice", |
|
| 6536 |
+ "libc", |
|
| 6537 |
+ "library-coreadi", |
|
| 6538 |
+ "log", |
|
| 6539 |
+ "ouroboros", |
|
| 6540 |
+ "plist", |
|
| 6541 |
+ "rand 0.10.2", |
|
| 6542 |
+ "reqwest", |
|
| 6543 |
+ "serde", |
|
| 6544 |
+ "tokio", |
|
| 6545 |
+ "toml 0.8.23", |
|
| 6546 |
+ "uuid", |
|
| 6483 | 6547 |
] |
| 6484 | 6548 |
|
| 6485 | 6549 |
[[package]] |
| ... | ... |
@@ -6919,9 +6983,21 @@ dependencies = [ |
| 6919 | 6983 |
"mio", |
| 6920 | 6984 |
"pin-project-lite", |
| 6921 | 6985 |
"socket2", |
| 6986 |
+ "tokio-macros", |
|
| 6922 | 6987 |
"windows-sys 0.61.2", |
| 6923 | 6988 |
] |
| 6924 | 6989 |
|
| 6990 |
+[[package]] |
|
| 6991 |
+name = "tokio-macros" |
|
| 6992 |
+version = "2.7.0" |
|
| 6993 |
+source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 6994 |
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" |
|
| 6995 |
+dependencies = [ |
|
| 6996 |
+ "proc-macro2", |
|
| 6997 |
+ "quote", |
|
| 6998 |
+ "syn", |
|
| 6999 |
+] |
|
| 7000 |
+ |
|
| 6925 | 7001 |
[[package]] |
| 6926 | 7002 |
name = "tokio-rustls" |
| 6927 | 7003 |
version = "0.26.4" |
| ... | ... |
@@ -6940,6 +7016,7 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" |
| 6940 | 7016 |
dependencies = [ |
| 6941 | 7017 |
"bytes", |
| 6942 | 7018 |
"futures-core", |
| 7019 |
+ "futures-io", |
|
| 6943 | 7020 |
"futures-sink", |
| 6944 | 7021 |
"pin-project-lite", |
| 6945 | 7022 |
"tokio", |
| ... | ... |
@@ -4,6 +4,32 @@ version = "0.1.0" |
| 4 | 4 |
edition = "2021" |
| 5 | 5 |
|
| 6 | 6 |
[dependencies] |
| 7 |
+adi.workspace = true |
|
| 8 |
+android-coreadi.workspace = true |
|
| 9 |
+async_zip = { version = "0.0.18", features = ["deflate", "tokio-fs"] }
|
|
| 10 |
+directories = "6.0.0" |
|
| 7 | 11 |
env_logger = "0.11.8" |
| 12 |
+futures-lite = "2.6.1" |
|
| 13 |
+grandslam.workspace = true |
|
| 8 | 14 |
gpui = { package = "gpui", git = "https://github.com/gpui-ce/gpui-ce" }
|
| 9 | 15 |
gpui_platform = { git = "https://github.com/gpui-ce/gpui-ce", features = ["font-kit", "x11", "wayland", "runtime_shaders"] }
|
| 16 |
+idevice = { version = "0.1.64", features = ["usbmuxd"] }
|
|
| 17 |
+futures = "0.3" |
|
| 18 |
+log = "0.4.33" |
|
| 19 |
+plist.workspace = true |
|
| 20 |
+rand = "0.10.2" |
|
| 21 |
+reqwest = { workspace = true, features = ["blocking"] }
|
|
| 22 |
+serde = { version = "1.0.228", features = ["derive"] }
|
|
| 23 |
+toml = "0.8.23" |
|
| 24 |
+tokio = { workspace = true, features = ["io-util", "net", "time"] }
|
|
| 25 |
+uuid = { version = "1.19", features = ["v4"] }
|
|
| 26 |
+ |
|
| 27 |
+[target.'cfg(target_os = "macos")'.dependencies] |
|
| 28 |
+adid-proxy = { path = "../adid-proxy" }
|
|
| 29 |
+core-foundation = "0.10.0" |
|
| 30 |
+libc = "0.2.176" |
|
| 31 |
+ |
|
| 32 |
+[target.'cfg(target_os = "windows")'.dependencies] |
|
| 33 |
+dlopen2 = "0.8.2" |
|
| 34 |
+library-coreadi.workspace = true |
|
| 35 |
+ouroboros = "0.18.5" |
| ... | ... |
@@ -0,0 +1,3 @@ |
| 1 |
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
|
| 2 |
+ <path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/> |
|
| 3 |
+</svg> |
| ... | ... |
@@ -0,0 +1,5 @@ |
| 1 |
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
|
| 2 |
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> |
|
| 3 |
+ <polyline points="16 17 21 12 16 7"/> |
|
| 4 |
+ <line x1="21" x2="9" y1="12" y2="12"/> |
|
| 5 |
+</svg> |
| ... | ... |
@@ -0,0 +1,4 @@ |
| 1 |
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
|
| 2 |
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/> |
|
| 3 |
+ <path d="M3 3v5h5"/> |
|
| 4 |
+</svg> |
| ... | ... |
@@ -0,0 +1,633 @@ |
| 1 |
+use crate::models::{
|
|
| 2 |
+ AdiBackendAvailability, AdiBackendDetail, AdiBackendKind, AdiBackendOption, |
|
| 3 |
+ AdiProvisioningState, AdiRepairAction, MachineIdentity, |
|
| 4 |
+}; |
|
| 5 |
+use crate::paths::app_data_dir; |
|
| 6 |
+use adi::core_adi::CoreADIADIProxy; |
|
| 7 |
+#[cfg(target_os = "windows")] |
|
| 8 |
+use adi::core_adi::{CoreADIParameters, CoreADIProxy};
|
|
| 9 |
+use adi::ADIProxy; |
|
| 10 |
+use async_zip::tokio::read::fs::ZipFileReader; |
|
| 11 |
+use futures_lite::io::AsyncReadExt; |
|
| 12 |
+use grandslam::bundle_information::BundleInformation; |
|
| 13 |
+use grandslam::device::Device; |
|
| 14 |
+use grandslam::http_session::AnisetteHTTPSession; |
|
| 15 |
+#[cfg(target_os = "windows")] |
|
| 16 |
+use ouroboros::self_referencing; |
|
| 17 |
+use std::ffi::CString; |
|
| 18 |
+use std::fs::{self, File};
|
|
| 19 |
+use std::io::{Read, Write};
|
|
| 20 |
+use std::path::{Path, PathBuf};
|
|
| 21 |
+ |
|
| 22 |
+const ANDROID_COREADI_APK_URL: &str = |
|
| 23 |
+ "https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk"; |
|
| 24 |
+const ANDROID_COREADI_APK_FILE: &str = "applemusic.apk"; |
|
| 25 |
+const ANDROID_COREADI_LIBRARY_FILE: &str = "libCoreADI.so"; |
|
| 26 |
+const GRANDSLAM_DSID: i64 = -2; |
|
| 27 |
+const XCODE_BUNDLE_INFORMATION: BundleInformation<'static> = BundleInformation {
|
|
| 28 |
+ bundle_name: "Xcode", |
|
| 29 |
+ bundle_identifier: "com.apple.dt.Xcode", |
|
| 30 |
+ bundle_version: "23792", |
|
| 31 |
+}; |
|
| 32 |
+ |
|
| 33 |
+#[derive(Clone, Copy, Debug)] |
|
| 34 |
+pub(crate) struct AndroidCoreAdiInstallProgress {
|
|
| 35 |
+ pub(crate) downloaded_bytes: u64, |
|
| 36 |
+ pub(crate) total_bytes: Option<u64>, |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+#[derive(Clone, Copy, Debug)] |
|
| 40 |
+pub(crate) enum AndroidCoreAdiInstallEvent {
|
|
| 41 |
+ Downloading(AndroidCoreAdiInstallProgress), |
|
| 42 |
+ Installing, |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+pub(crate) fn available_adi_backends(android_adi_identifier: &str) -> Vec<AdiBackendOption> {
|
|
| 46 |
+ let mut backends: Vec<_> = vec![ |
|
| 47 |
+ system_adi_backend(), |
|
| 48 |
+ windows_coreadi_backend(), |
|
| 49 |
+ android_coreadi_backend(android_adi_identifier), |
|
| 50 |
+ ] |
|
| 51 |
+ .into_iter() |
|
| 52 |
+ .filter(|backend| backend.availability.is_available()) |
|
| 53 |
+ .collect(); |
|
| 54 |
+ |
|
| 55 |
+ for backend in &mut backends {
|
|
| 56 |
+ backend.provisioning_state = if backend.availability.is_ready() {
|
|
| 57 |
+ adi_provisioning_state(backend.kind, android_adi_identifier) |
|
| 58 |
+ } else {
|
|
| 59 |
+ AdiProvisioningState::NotAvailable |
|
| 60 |
+ }; |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ backends |
|
| 64 |
+} |
|
| 65 |
+ |
|
| 66 |
+pub(crate) fn default_adi_backend(backends: &[AdiBackendOption]) -> usize {
|
|
| 67 |
+ backends |
|
| 68 |
+ .iter() |
|
| 69 |
+ .position(|backend| backend.availability.is_ready()) |
|
| 70 |
+ .unwrap_or(0) |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 73 |
+#[allow(dead_code)] |
|
| 74 |
+pub(crate) fn selected_adi_proxy( |
|
| 75 |
+ kind: AdiBackendKind, |
|
| 76 |
+ android_adi_identifier: &str, |
|
| 77 |
+) -> Result<Box<dyn ADIProxy>, String> {
|
|
| 78 |
+ match kind {
|
|
| 79 |
+ AdiBackendKind::SystemAdid => system_adi_proxy(), |
|
| 80 |
+ AdiBackendKind::WindowsCoreAdi => windows_coreadi_proxy(), |
|
| 81 |
+ AdiBackendKind::AndroidCoreAdi => android_coreadi_proxy(android_adi_identifier), |
|
| 82 |
+ } |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+#[allow(dead_code)] |
|
| 86 |
+pub(crate) fn erase_adi_provisioning( |
|
| 87 |
+ kind: AdiBackendKind, |
|
| 88 |
+ android_adi_identifier: &str, |
|
| 89 |
+) -> Result<(), String> {
|
|
| 90 |
+ selected_adi_proxy(kind, android_adi_identifier)? |
|
| 91 |
+ .erase_provisioning(GRANDSLAM_DSID) |
|
| 92 |
+ .map_err(|error| format!("Failed to erase ADI provisioning: {error}"))
|
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+pub(crate) fn provision_adi( |
|
| 96 |
+ kind: AdiBackendKind, |
|
| 97 |
+ machine_identity: &MachineIdentity, |
|
| 98 |
+ android_adi_identifier: &str, |
|
| 99 |
+) -> Result<(), String> {
|
|
| 100 |
+ tokio::runtime::Builder::new_current_thread() |
|
| 101 |
+ .enable_all() |
|
| 102 |
+ .build() |
|
| 103 |
+ .map_err(|error| format!("Failed to start ADI provisioning runtime: {error}"))?
|
|
| 104 |
+ .block_on(provision_adi_async( |
|
| 105 |
+ kind, |
|
| 106 |
+ machine_identity, |
|
| 107 |
+ android_adi_identifier, |
|
| 108 |
+ )) |
|
| 109 |
+} |
|
| 110 |
+ |
|
| 111 |
+async fn provision_adi_async( |
|
| 112 |
+ kind: AdiBackendKind, |
|
| 113 |
+ machine_identity: &MachineIdentity, |
|
| 114 |
+ android_adi_identifier: &str, |
|
| 115 |
+) -> Result<(), String> {
|
|
| 116 |
+ let proxy = selected_adi_proxy(kind, android_adi_identifier)?; |
|
| 117 |
+ let http_session = |
|
| 118 |
+ grandslam::http_session(grandslam_device(machine_identity), XCODE_BUNDLE_INFORMATION) |
|
| 119 |
+ .await |
|
| 120 |
+ .map_err(|error| format!("Failed to create ADI provisioning session: {error}"))?;
|
|
| 121 |
+ let anisette_session = AnisetteHTTPSession::new(http_session, proxy.as_ref()); |
|
| 122 |
+ |
|
| 123 |
+ grandslam::provision(&anisette_session) |
|
| 124 |
+ .await |
|
| 125 |
+ .map_err(|error| format!("Failed to provision ADI: {error}"))
|
|
| 126 |
+} |
|
| 127 |
+ |
|
| 128 |
+fn grandslam_device(machine_identity: &MachineIdentity) -> Device {
|
|
| 129 |
+ Device {
|
|
| 130 |
+ device_model: machine_identity.machine_name.to_string(), |
|
| 131 |
+ operating_system_information: format!( |
|
| 132 |
+ "{};{}",
|
|
| 133 |
+ machine_identity.os_name, machine_identity.os_version |
|
| 134 |
+ ), |
|
| 135 |
+ device_uuid: machine_identity.machine_id.to_string(), |
|
| 136 |
+ } |
|
| 137 |
+} |
|
| 138 |
+ |
|
| 139 |
+fn adi_provisioning_state( |
|
| 140 |
+ kind: AdiBackendKind, |
|
| 141 |
+ android_adi_identifier: &str, |
|
| 142 |
+) -> AdiProvisioningState {
|
|
| 143 |
+ match selected_adi_proxy(kind, android_adi_identifier) {
|
|
| 144 |
+ Ok(proxy) => match proxy.is_machine_provisioned(GRANDSLAM_DSID) {
|
|
| 145 |
+ Ok(true) => AdiProvisioningState::Provisioned, |
|
| 146 |
+ Ok(false) => AdiProvisioningState::NotProvisioned, |
|
| 147 |
+ Err(error) => AdiProvisioningState::Error(error.to_string().into()), |
|
| 148 |
+ }, |
|
| 149 |
+ Err(error) => AdiProvisioningState::Error(error.into()), |
|
| 150 |
+ } |
|
| 151 |
+} |
|
| 152 |
+ |
|
| 153 |
+#[cfg(target_os = "macos")] |
|
| 154 |
+fn system_adi_proxy() -> Result<Box<dyn ADIProxy>, String> {
|
|
| 155 |
+ Ok(Box::new(adid_proxy::ADIdProxy::connect())) |
|
| 156 |
+} |
|
| 157 |
+ |
|
| 158 |
+#[cfg(not(target_os = "macos"))] |
|
| 159 |
+fn system_adi_proxy() -> Result<Box<dyn ADIProxy>, String> {
|
|
| 160 |
+ Err("System ADI is only available on macOS.".to_string())
|
|
| 161 |
+} |
|
| 162 |
+ |
|
| 163 |
+#[cfg(target_os = "windows")] |
|
| 164 |
+#[self_referencing] |
|
| 165 |
+struct WindowsCoreADIProxy {
|
|
| 166 |
+ library: dlopen2::symbor::Library, |
|
| 167 |
+ |
|
| 168 |
+ #[borrows(library)] |
|
| 169 |
+ #[covariant] |
|
| 170 |
+ proxy: library_coreadi::LibraryCoreADIProxy<'this>, |
|
| 171 |
+} |
|
| 172 |
+ |
|
| 173 |
+#[cfg(target_os = "windows")] |
|
| 174 |
+impl WindowsCoreADIProxy {
|
|
| 175 |
+ fn open(path: PathBuf) -> Result<Self, String> {
|
|
| 176 |
+ let library = dlopen2::symbor::Library::open(path.as_os_str()) |
|
| 177 |
+ .map_err(|error| format!("Failed to load CoreADI.dll: {error}"))?;
|
|
| 178 |
+ let proxy = WindowsCoreADIProxyTryBuilder {
|
|
| 179 |
+ library, |
|
| 180 |
+ proxy_builder: |library| {
|
|
| 181 |
+ library_coreadi::LibraryCoreADIProxy::new(library) |
|
| 182 |
+ .map_err(|error| format!("CoreADI entry point could not be loaded: {error}"))
|
|
| 183 |
+ }, |
|
| 184 |
+ } |
|
| 185 |
+ .try_build()?; |
|
| 186 |
+ |
|
| 187 |
+ proxy |
|
| 188 |
+ .with_proxy(|proxy| {
|
|
| 189 |
+ CoreADIADIProxy::initialize(proxy) |
|
| 190 |
+ .map_err(|error| format!("CoreADI initialization failed: {error}"))
|
|
| 191 |
+ }) |
|
| 192 |
+ .map(|()| proxy) |
|
| 193 |
+ } |
|
| 194 |
+} |
|
| 195 |
+ |
|
| 196 |
+#[cfg(target_os = "windows")] |
|
| 197 |
+impl CoreADIProxy for WindowsCoreADIProxy {
|
|
| 198 |
+ unsafe fn dispatch(&self, function_code: u32, parameters: *const CoreADIParameters) -> i32 {
|
|
| 199 |
+ self.with_proxy(|proxy| unsafe { proxy.dispatch(function_code, parameters) })
|
|
| 200 |
+ } |
|
| 201 |
+} |
|
| 202 |
+ |
|
| 203 |
+#[cfg(target_os = "windows")] |
|
| 204 |
+fn windows_coreadi_proxy() -> Result<Box<dyn ADIProxy>, String> {
|
|
| 205 |
+ let library_path = find_windows_coreadi_library().ok_or_else(|| {
|
|
| 206 |
+ "CoreADI.dll was not found in the usual iTunes or iCloud folders.".to_string() |
|
| 207 |
+ })?; |
|
| 208 |
+ Ok(Box::new(WindowsCoreADIProxy::open(library_path)?)) |
|
| 209 |
+} |
|
| 210 |
+ |
|
| 211 |
+#[cfg(not(target_os = "windows"))] |
|
| 212 |
+fn windows_coreadi_proxy() -> Result<Box<dyn ADIProxy>, String> {
|
|
| 213 |
+ Err("iTunes or iCloud CoreADI is only available on Windows.".to_string())
|
|
| 214 |
+} |
|
| 215 |
+ |
|
| 216 |
+fn android_coreadi_proxy(android_adi_identifier: &str) -> Result<Box<dyn ADIProxy>, String> {
|
|
| 217 |
+ let library_path = android_coreadi_library_path() |
|
| 218 |
+ .ok_or_else(|| "The application data folder is not available.".to_string())?; |
|
| 219 |
+ let library_data = fs::read(&library_path).map_err(|error| {
|
|
| 220 |
+ format!( |
|
| 221 |
+ "Failed to read Apple Music CoreADI at {}: {error}",
|
|
| 222 |
+ library_path.display() |
|
| 223 |
+ ) |
|
| 224 |
+ })?; |
|
| 225 |
+ |
|
| 226 |
+ let proxy = android_coreadi::AndroidCoreADIProxy::load_library(library_data) |
|
| 227 |
+ .map_err(|error| format!("Failed to load Apple Music CoreADI: {error}"))?;
|
|
| 228 |
+ CoreADIADIProxy::initialize(&proxy) |
|
| 229 |
+ .map_err(|error| format!("Apple Music CoreADI initialization failed: {error}"))?;
|
|
| 230 |
+ |
|
| 231 |
+ let provisioning_path = android_coreadi_provisioning_path() |
|
| 232 |
+ .ok_or_else(|| "The application data folder is not available.".to_string())?; |
|
| 233 |
+ fs::create_dir_all(&provisioning_path).map_err(|error| {
|
|
| 234 |
+ format!( |
|
| 235 |
+ "Failed to create CoreADI provisioning folder at {}: {error}",
|
|
| 236 |
+ provisioning_path.display() |
|
| 237 |
+ ) |
|
| 238 |
+ })?; |
|
| 239 |
+ let provisioning_path = c_string_from_path(&provisioning_path)?; |
|
| 240 |
+ CoreADIADIProxy::set_provisioning_path(&proxy, &provisioning_path) |
|
| 241 |
+ .map_err(|error| format!("Failed to configure CoreADI provisioning path: {error}"))?;
|
|
| 242 |
+ |
|
| 243 |
+ if android_adi_identifier.len() != 16 {
|
|
| 244 |
+ return Err(format!( |
|
| 245 |
+ "Apple Music CoreADI ADI identifier must be 16 characters long, got {}.",
|
|
| 246 |
+ android_adi_identifier.len() |
|
| 247 |
+ )); |
|
| 248 |
+ } |
|
| 249 |
+ CoreADIADIProxy::set_android_id(&proxy, android_adi_identifier) |
|
| 250 |
+ .map_err(|error| format!("Failed to configure CoreADI ADI identifier: {error}"))?;
|
|
| 251 |
+ |
|
| 252 |
+ Ok(Box::new(proxy)) |
|
| 253 |
+} |
|
| 254 |
+ |
|
| 255 |
+fn system_adi_backend() -> AdiBackendOption {
|
|
| 256 |
+ #[cfg(target_os = "macos")] |
|
| 257 |
+ let availability = AdiBackendAvailability::Ready; |
|
| 258 |
+ |
|
| 259 |
+ #[cfg(not(target_os = "macos"))] |
|
| 260 |
+ let availability = AdiBackendAvailability::Unavailable; |
|
| 261 |
+ |
|
| 262 |
+ AdiBackendOption {
|
|
| 263 |
+ kind: AdiBackendKind::SystemAdid, |
|
| 264 |
+ name: "System ADI".into(), |
|
| 265 |
+ detail: "Use the platform ADI provider.".into(), |
|
| 266 |
+ availability, |
|
| 267 |
+ details: vec![AdiBackendDetail {
|
|
| 268 |
+ label: "Identity storage".into(), |
|
| 269 |
+ value: "System managed".into(), |
|
| 270 |
+ }], |
|
| 271 |
+ provisioning_state: AdiProvisioningState::Unknown, |
|
| 272 |
+ editable_identity: false, |
|
| 273 |
+ repair_action: None, |
|
| 274 |
+ } |
|
| 275 |
+} |
|
| 276 |
+ |
|
| 277 |
+fn windows_coreadi_backend() -> AdiBackendOption {
|
|
| 278 |
+ let library_path = find_windows_coreadi_library(); |
|
| 279 |
+ let availability = if cfg!(target_os = "windows") {
|
|
| 280 |
+ if library_path.is_some() {
|
|
| 281 |
+ AdiBackendAvailability::Ready |
|
| 282 |
+ } else {
|
|
| 283 |
+ AdiBackendAvailability::NeedsSetup |
|
| 284 |
+ } |
|
| 285 |
+ } else {
|
|
| 286 |
+ AdiBackendAvailability::Unavailable |
|
| 287 |
+ }; |
|
| 288 |
+ |
|
| 289 |
+ AdiBackendOption {
|
|
| 290 |
+ kind: AdiBackendKind::WindowsCoreAdi, |
|
| 291 |
+ name: "iTunes / iCloud CoreADI".into(), |
|
| 292 |
+ detail: "Load Apple's Windows ADI library.".into(), |
|
| 293 |
+ availability, |
|
| 294 |
+ details: vec![ |
|
| 295 |
+ AdiBackendDetail {
|
|
| 296 |
+ label: "Library path".into(), |
|
| 297 |
+ value: library_path |
|
| 298 |
+ .as_ref() |
|
| 299 |
+ .map(|path| path.display().to_string()) |
|
| 300 |
+ .unwrap_or_else(|| "Not found".to_string()) |
|
| 301 |
+ .into(), |
|
| 302 |
+ }, |
|
| 303 |
+ AdiBackendDetail {
|
|
| 304 |
+ label: "Identity storage".into(), |
|
| 305 |
+ value: "Apple managed".into(), |
|
| 306 |
+ }, |
|
| 307 |
+ ], |
|
| 308 |
+ provisioning_state: AdiProvisioningState::Unknown, |
|
| 309 |
+ editable_identity: false, |
|
| 310 |
+ repair_action: (availability == AdiBackendAvailability::NeedsSetup) |
|
| 311 |
+ .then_some(AdiRepairAction::LocateLibrary), |
|
| 312 |
+ } |
|
| 313 |
+} |
|
| 314 |
+ |
|
| 315 |
+fn android_coreadi_backend(android_adi_identifier: &str) -> AdiBackendOption {
|
|
| 316 |
+ let Some(abi) = android_coreadi_abi() else {
|
|
| 317 |
+ return AdiBackendOption {
|
|
| 318 |
+ kind: AdiBackendKind::AndroidCoreAdi, |
|
| 319 |
+ name: "Apple Music CoreADI".into(), |
|
| 320 |
+ detail: "Use Android's portable CoreADI library.".into(), |
|
| 321 |
+ availability: AdiBackendAvailability::Unavailable, |
|
| 322 |
+ details: vec![AdiBackendDetail {
|
|
| 323 |
+ label: "Architecture".into(), |
|
| 324 |
+ value: "Unsupported".into(), |
|
| 325 |
+ }], |
|
| 326 |
+ provisioning_state: AdiProvisioningState::NotAvailable, |
|
| 327 |
+ editable_identity: false, |
|
| 328 |
+ repair_action: None, |
|
| 329 |
+ }; |
|
| 330 |
+ }; |
|
| 331 |
+ |
|
| 332 |
+ let library_path = android_coreadi_library_path(); |
|
| 333 |
+ let availability = if library_path.as_ref().is_some_and(|path| path.exists()) {
|
|
| 334 |
+ AdiBackendAvailability::Ready |
|
| 335 |
+ } else {
|
|
| 336 |
+ AdiBackendAvailability::NeedsSetup |
|
| 337 |
+ }; |
|
| 338 |
+ let library_path_label = library_path |
|
| 339 |
+ .as_ref() |
|
| 340 |
+ .map(|path| path.display().to_string()) |
|
| 341 |
+ .unwrap_or_else(|| "Not available".to_string()); |
|
| 342 |
+ let provisioning_path_label = android_coreadi_provisioning_path() |
|
| 343 |
+ .map(|path| path.display().to_string()) |
|
| 344 |
+ .unwrap_or_else(|| "Not available".to_string()); |
|
| 345 |
+ |
|
| 346 |
+ AdiBackendOption {
|
|
| 347 |
+ kind: AdiBackendKind::AndroidCoreAdi, |
|
| 348 |
+ name: "Apple Music CoreADI".into(), |
|
| 349 |
+ detail: "Use Android's portable CoreADI library.".into(), |
|
| 350 |
+ availability, |
|
| 351 |
+ details: vec![ |
|
| 352 |
+ AdiBackendDetail {
|
|
| 353 |
+ label: "Library path".into(), |
|
| 354 |
+ value: library_path_label.into(), |
|
| 355 |
+ }, |
|
| 356 |
+ AdiBackendDetail {
|
|
| 357 |
+ label: "APK ABI".into(), |
|
| 358 |
+ value: abi.into(), |
|
| 359 |
+ }, |
|
| 360 |
+ AdiBackendDetail {
|
|
| 361 |
+ label: "ADI identifier".into(), |
|
| 362 |
+ value: android_adi_identifier.to_string().into(), |
|
| 363 |
+ }, |
|
| 364 |
+ AdiBackendDetail {
|
|
| 365 |
+ label: "Provisioning path".into(), |
|
| 366 |
+ value: provisioning_path_label.into(), |
|
| 367 |
+ }, |
|
| 368 |
+ ], |
|
| 369 |
+ provisioning_state: if availability.is_ready() {
|
|
| 370 |
+ AdiProvisioningState::Unknown |
|
| 371 |
+ } else {
|
|
| 372 |
+ AdiProvisioningState::NotAvailable |
|
| 373 |
+ }, |
|
| 374 |
+ editable_identity: true, |
|
| 375 |
+ repair_action: (availability == AdiBackendAvailability::NeedsSetup) |
|
| 376 |
+ .then_some(AdiRepairAction::InstallCoreAdi), |
|
| 377 |
+ } |
|
| 378 |
+} |
|
| 379 |
+ |
|
| 380 |
+pub(crate) fn android_coreadi_library_path() -> Option<PathBuf> {
|
|
| 381 |
+ app_data_dir() |
|
| 382 |
+ .zip(android_coreadi_abi()) |
|
| 383 |
+ .map(|(path, abi)| path.join(abi).join(ANDROID_COREADI_LIBRARY_FILE)) |
|
| 384 |
+} |
|
| 385 |
+ |
|
| 386 |
+fn android_coreadi_apk_path() -> Option<PathBuf> {
|
|
| 387 |
+ app_data_dir().map(|path| path.join(ANDROID_COREADI_APK_FILE)) |
|
| 388 |
+} |
|
| 389 |
+ |
|
| 390 |
+fn android_coreadi_provisioning_path() -> Option<PathBuf> {
|
|
| 391 |
+ app_data_dir().map(|path| path.join("coreadi"))
|
|
| 392 |
+} |
|
| 393 |
+ |
|
| 394 |
+pub(crate) fn download_android_coreadi_apk( |
|
| 395 |
+ mut progress: impl FnMut(AndroidCoreAdiInstallProgress), |
|
| 396 |
+) -> Result<PathBuf, String> {
|
|
| 397 |
+ let _ = android_coreadi_abi().ok_or_else(|| {
|
|
| 398 |
+ format!( |
|
| 399 |
+ "Apple Music CoreADI is not available for this CPU architecture ({})",
|
|
| 400 |
+ std::env::consts::ARCH |
|
| 401 |
+ ) |
|
| 402 |
+ })?; |
|
| 403 |
+ let destination = android_coreadi_apk_path() |
|
| 404 |
+ .ok_or_else(|| "The application data folder is not available.".to_string())?; |
|
| 405 |
+ if let Some(parent) = destination.parent() {
|
|
| 406 |
+ fs::create_dir_all(parent).map_err(|error| {
|
|
| 407 |
+ format!( |
|
| 408 |
+ "Failed to create Apple Music APK folder at {}: {error}",
|
|
| 409 |
+ parent.display() |
|
| 410 |
+ ) |
|
| 411 |
+ })?; |
|
| 412 |
+ } |
|
| 413 |
+ |
|
| 414 |
+ let mut response = reqwest::blocking::Client::new() |
|
| 415 |
+ .get(ANDROID_COREADI_APK_URL) |
|
| 416 |
+ .send() |
|
| 417 |
+ .and_then(|response| response.error_for_status()) |
|
| 418 |
+ .map_err(|error| format!("Failed to download Apple Music APK: {error}"))?;
|
|
| 419 |
+ let total_bytes = response.content_length(); |
|
| 420 |
+ let mut file = File::create(&destination).map_err(|error| {
|
|
| 421 |
+ format!( |
|
| 422 |
+ "Failed to create Apple Music APK at {}: {error}",
|
|
| 423 |
+ destination.display() |
|
| 424 |
+ ) |
|
| 425 |
+ })?; |
|
| 426 |
+ let mut downloaded_bytes = 0; |
|
| 427 |
+ let mut buffer = [0; 64 * 1024]; |
|
| 428 |
+ progress(AndroidCoreAdiInstallProgress {
|
|
| 429 |
+ downloaded_bytes, |
|
| 430 |
+ total_bytes, |
|
| 431 |
+ }); |
|
| 432 |
+ |
|
| 433 |
+ loop {
|
|
| 434 |
+ let read = response |
|
| 435 |
+ .read(&mut buffer) |
|
| 436 |
+ .map_err(|error| format!("Failed to download Apple Music APK: {error}"))?;
|
|
| 437 |
+ if read == 0 {
|
|
| 438 |
+ break; |
|
| 439 |
+ } |
|
| 440 |
+ |
|
| 441 |
+ file.write_all(&buffer[..read]) |
|
| 442 |
+ .map_err(|error| format!("Failed to save Apple Music APK: {error}"))?;
|
|
| 443 |
+ downloaded_bytes += read as u64; |
|
| 444 |
+ progress(AndroidCoreAdiInstallProgress {
|
|
| 445 |
+ downloaded_bytes, |
|
| 446 |
+ total_bytes, |
|
| 447 |
+ }); |
|
| 448 |
+ } |
|
| 449 |
+ |
|
| 450 |
+ file.flush().map_err(|error| {
|
|
| 451 |
+ format!( |
|
| 452 |
+ "Failed to flush Apple Music APK at {}: {error}",
|
|
| 453 |
+ destination.display() |
|
| 454 |
+ ) |
|
| 455 |
+ })?; |
|
| 456 |
+ |
|
| 457 |
+ if downloaded_bytes == 0 {
|
|
| 458 |
+ return Err("Downloaded Apple Music APK is empty.".to_string());
|
|
| 459 |
+ } |
|
| 460 |
+ |
|
| 461 |
+ Ok(destination) |
|
| 462 |
+} |
|
| 463 |
+ |
|
| 464 |
+pub(crate) fn download_and_install_android_coreadi( |
|
| 465 |
+ mut progress: impl FnMut(AndroidCoreAdiInstallEvent), |
|
| 466 |
+) -> Result<PathBuf, String> {
|
|
| 467 |
+ let apk_path = download_android_coreadi_apk(|download| {
|
|
| 468 |
+ progress(AndroidCoreAdiInstallEvent::Downloading(download)); |
|
| 469 |
+ })?; |
|
| 470 |
+ progress(AndroidCoreAdiInstallEvent::Installing); |
|
| 471 |
+ install_android_coreadi_from_apk(&apk_path) |
|
| 472 |
+} |
|
| 473 |
+ |
|
| 474 |
+#[allow(dead_code)] |
|
| 475 |
+pub(crate) fn install_android_coreadi_from_apk(apk_path: &Path) -> Result<PathBuf, String> {
|
|
| 476 |
+ tokio::runtime::Builder::new_current_thread() |
|
| 477 |
+ .enable_all() |
|
| 478 |
+ .build() |
|
| 479 |
+ .map_err(|error| format!("Failed to start CoreADI APK reader runtime: {error}"))?
|
|
| 480 |
+ .block_on(install_android_coreadi_from_apk_async(apk_path)) |
|
| 481 |
+} |
|
| 482 |
+ |
|
| 483 |
+async fn install_android_coreadi_from_apk_async(apk_path: &Path) -> Result<PathBuf, String> {
|
|
| 484 |
+ let entry_name = android_coreadi_apk_entry().ok_or_else(|| {
|
|
| 485 |
+ format!( |
|
| 486 |
+ "Apple Music CoreADI is not available for this CPU architecture ({})",
|
|
| 487 |
+ std::env::consts::ARCH |
|
| 488 |
+ ) |
|
| 489 |
+ })?; |
|
| 490 |
+ let reader = ZipFileReader::new(apk_path).await.map_err(|error| {
|
|
| 491 |
+ format!( |
|
| 492 |
+ "Failed to read APK archive at {}: {error}",
|
|
| 493 |
+ apk_path.display() |
|
| 494 |
+ ) |
|
| 495 |
+ })?; |
|
| 496 |
+ let entry_index = reader |
|
| 497 |
+ .file() |
|
| 498 |
+ .entries() |
|
| 499 |
+ .iter() |
|
| 500 |
+ .position(|entry| {
|
|
| 501 |
+ entry |
|
| 502 |
+ .filename() |
|
| 503 |
+ .as_str() |
|
| 504 |
+ .is_ok_and(|filename| filename == entry_name) |
|
| 505 |
+ }) |
|
| 506 |
+ .ok_or_else(|| format!("APK does not contain {entry_name}."))?;
|
|
| 507 |
+ |
|
| 508 |
+ let mut entry_reader = reader |
|
| 509 |
+ .reader_without_entry(entry_index) |
|
| 510 |
+ .await |
|
| 511 |
+ .map_err(|error| format!("Failed to read CoreADI from APK: {error}"))?;
|
|
| 512 |
+ |
|
| 513 |
+ let destination = android_coreadi_library_path() |
|
| 514 |
+ .ok_or_else(|| "The application data folder is not available.".to_string())?; |
|
| 515 |
+ if let Some(parent) = destination.parent() {
|
|
| 516 |
+ fs::create_dir_all(parent).map_err(|error| {
|
|
| 517 |
+ format!( |
|
| 518 |
+ "Failed to create CoreADI folder at {}: {error}",
|
|
| 519 |
+ parent.display() |
|
| 520 |
+ ) |
|
| 521 |
+ })?; |
|
| 522 |
+ } |
|
| 523 |
+ let mut destination_file = File::create(&destination).map_err(|error| {
|
|
| 524 |
+ format!( |
|
| 525 |
+ "Failed to create CoreADI destination at {}: {error}",
|
|
| 526 |
+ destination.display() |
|
| 527 |
+ ) |
|
| 528 |
+ })?; |
|
| 529 |
+ let mut buffer = [0u8; 64 * 1024]; |
|
| 530 |
+ loop {
|
|
| 531 |
+ let read = entry_reader |
|
| 532 |
+ .read(&mut buffer) |
|
| 533 |
+ .await |
|
| 534 |
+ .map_err(|error| format!("Failed to extract CoreADI from APK: {error}"))?;
|
|
| 535 |
+ if read == 0 {
|
|
| 536 |
+ break; |
|
| 537 |
+ } |
|
| 538 |
+ destination_file |
|
| 539 |
+ .write_all(&buffer[..read]) |
|
| 540 |
+ .map_err(|error| {
|
|
| 541 |
+ format!( |
|
| 542 |
+ "Failed to store CoreADI at {}: {error}",
|
|
| 543 |
+ destination.display() |
|
| 544 |
+ ) |
|
| 545 |
+ })?; |
|
| 546 |
+ } |
|
| 547 |
+ destination_file.flush().map_err(|error| {
|
|
| 548 |
+ format!( |
|
| 549 |
+ "Failed to flush CoreADI at {}: {error}",
|
|
| 550 |
+ destination.display() |
|
| 551 |
+ ) |
|
| 552 |
+ })?; |
|
| 553 |
+ |
|
| 554 |
+ Ok(destination) |
|
| 555 |
+} |
|
| 556 |
+ |
|
| 557 |
+#[cfg(target_arch = "aarch64")] |
|
| 558 |
+fn android_coreadi_abi() -> Option<&'static str> {
|
|
| 559 |
+ Some("arm64-v8a")
|
|
| 560 |
+} |
|
| 561 |
+ |
|
| 562 |
+#[cfg(target_arch = "arm")] |
|
| 563 |
+fn android_coreadi_abi() -> Option<&'static str> {
|
|
| 564 |
+ Some("armeabi-v7a")
|
|
| 565 |
+} |
|
| 566 |
+ |
|
| 567 |
+#[cfg(target_arch = "x86")] |
|
| 568 |
+fn android_coreadi_abi() -> Option<&'static str> {
|
|
| 569 |
+ Some("x86")
|
|
| 570 |
+} |
|
| 571 |
+ |
|
| 572 |
+#[cfg(target_arch = "x86_64")] |
|
| 573 |
+fn android_coreadi_abi() -> Option<&'static str> {
|
|
| 574 |
+ Some("x86_64")
|
|
| 575 |
+} |
|
| 576 |
+ |
|
| 577 |
+#[cfg(not(any( |
|
| 578 |
+ target_arch = "aarch64", |
|
| 579 |
+ target_arch = "arm", |
|
| 580 |
+ target_arch = "x86", |
|
| 581 |
+ target_arch = "x86_64" |
|
| 582 |
+)))] |
|
| 583 |
+fn android_coreadi_abi() -> Option<&'static str> {
|
|
| 584 |
+ None |
|
| 585 |
+} |
|
| 586 |
+ |
|
| 587 |
+fn android_coreadi_apk_entry() -> Option<String> {
|
|
| 588 |
+ android_coreadi_abi().map(|abi| format!("lib/{abi}/{ANDROID_COREADI_LIBRARY_FILE}"))
|
|
| 589 |
+} |
|
| 590 |
+ |
|
| 591 |
+fn c_string_from_path(path: &Path) -> Result<CString, String> {
|
|
| 592 |
+ CString::new(path.to_string_lossy().into_owned()) |
|
| 593 |
+ .map_err(|_| format!("Path contains a null byte: {}", path.display()))
|
|
| 594 |
+} |
|
| 595 |
+ |
|
| 596 |
+fn find_windows_coreadi_library() -> Option<PathBuf> {
|
|
| 597 |
+ if !cfg!(target_os = "windows") {
|
|
| 598 |
+ return None; |
|
| 599 |
+ } |
|
| 600 |
+ |
|
| 601 |
+ windows_coreadi_candidates() |
|
| 602 |
+ .into_iter() |
|
| 603 |
+ .find(|path| path.exists()) |
|
| 604 |
+} |
|
| 605 |
+ |
|
| 606 |
+fn windows_coreadi_candidates() -> Vec<PathBuf> {
|
|
| 607 |
+ let mut candidates = Vec::new(); |
|
| 608 |
+ for env_var in ["ProgramFiles", "ProgramFiles(x86)"] {
|
|
| 609 |
+ if let Some(root) = std::env::var_os(env_var) {
|
|
| 610 |
+ let root = PathBuf::from(root); |
|
| 611 |
+ candidates.push( |
|
| 612 |
+ root.join("Common Files")
|
|
| 613 |
+ .join("Apple")
|
|
| 614 |
+ .join("Apple Application Support")
|
|
| 615 |
+ .join("CoreADI.dll"),
|
|
| 616 |
+ ); |
|
| 617 |
+ candidates.push( |
|
| 618 |
+ root.join("Apple")
|
|
| 619 |
+ .join("Apple Application Support")
|
|
| 620 |
+ .join("CoreADI.dll"),
|
|
| 621 |
+ ); |
|
| 622 |
+ } |
|
| 623 |
+ } |
|
| 624 |
+ if let Some(root) = std::env::var_os("CommonProgramFiles") {
|
|
| 625 |
+ candidates.push( |
|
| 626 |
+ PathBuf::from(root) |
|
| 627 |
+ .join("Apple")
|
|
| 628 |
+ .join("Apple Application Support")
|
|
| 629 |
+ .join("CoreADI.dll"),
|
|
| 630 |
+ ); |
|
| 631 |
+ } |
|
| 632 |
+ candidates |
|
| 633 |
+} |
| ... | ... |
@@ -0,0 +1,58 @@ |
| 1 |
+use crate::adi_backend; |
|
| 2 |
+use crate::models::{AdiBackendKind, AdiBackendOption, MachineIdentity};
|
|
| 3 |
+use std::path::{Path, PathBuf};
|
|
| 4 |
+ |
|
| 5 |
+#[derive(Clone, Debug)] |
|
| 6 |
+pub(crate) struct CoreAdiInstallProgress {
|
|
| 7 |
+ pub(crate) downloaded_bytes: u64, |
|
| 8 |
+ pub(crate) total_bytes: Option<u64>, |
|
| 9 |
+} |
|
| 10 |
+ |
|
| 11 |
+#[derive(Clone, Debug)] |
|
| 12 |
+pub(crate) enum CoreAdiInstallEvent {
|
|
| 13 |
+ Downloading(CoreAdiInstallProgress), |
|
| 14 |
+ Installing, |
|
| 15 |
+} |
|
| 16 |
+ |
|
| 17 |
+pub(crate) fn available_backends(android_adi_identifier: &str) -> Vec<AdiBackendOption> {
|
|
| 18 |
+ adi_backend::available_adi_backends(android_adi_identifier) |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+pub(crate) fn default_backend(backends: &[AdiBackendOption]) -> usize {
|
|
| 22 |
+ adi_backend::default_adi_backend(backends) |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 25 |
+pub(crate) fn download_and_install_coreadi( |
|
| 26 |
+ mut progress: impl FnMut(CoreAdiInstallEvent), |
|
| 27 |
+) -> Result<PathBuf, String> {
|
|
| 28 |
+ adi_backend::download_and_install_android_coreadi(move |event| {
|
|
| 29 |
+ progress(match event {
|
|
| 30 |
+ adi_backend::AndroidCoreAdiInstallEvent::Downloading(progress) => {
|
|
| 31 |
+ CoreAdiInstallEvent::Downloading(CoreAdiInstallProgress {
|
|
| 32 |
+ downloaded_bytes: progress.downloaded_bytes, |
|
| 33 |
+ total_bytes: progress.total_bytes, |
|
| 34 |
+ }) |
|
| 35 |
+ } |
|
| 36 |
+ adi_backend::AndroidCoreAdiInstallEvent::Installing => CoreAdiInstallEvent::Installing, |
|
| 37 |
+ }); |
|
| 38 |
+ }) |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+pub(crate) fn install_coreadi_from_apk(apk_path: &Path) -> Result<PathBuf, String> {
|
|
| 42 |
+ adi_backend::install_android_coreadi_from_apk(apk_path) |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+pub(crate) fn erase_provisioning( |
|
| 46 |
+ kind: AdiBackendKind, |
|
| 47 |
+ android_adi_identifier: &str, |
|
| 48 |
+) -> Result<(), String> {
|
|
| 49 |
+ adi_backend::erase_adi_provisioning(kind, android_adi_identifier) |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+pub(crate) fn provision( |
|
| 53 |
+ kind: AdiBackendKind, |
|
| 54 |
+ machine_identity: &MachineIdentity, |
|
| 55 |
+ android_adi_identifier: &str, |
|
| 56 |
+) -> Result<(), String> {
|
|
| 57 |
+ adi_backend::provision_adi(kind, machine_identity, android_adi_identifier) |
|
| 58 |
+} |
| ... | ... |
@@ -0,0 +1,137 @@ |
| 1 |
+use crate::ipa::read_ipa; |
|
| 2 |
+use crate::models::{AppOption, PatchOption};
|
|
| 3 |
+use crate::preferences::{apply_app_overrides, AppPreferences};
|
|
| 4 |
+use std::path::{Path, PathBuf};
|
|
| 5 |
+ |
|
| 6 |
+#[derive(Debug, Default)] |
|
| 7 |
+pub(crate) struct AppSelection {
|
|
| 8 |
+ apps: Vec<AppOption>, |
|
| 9 |
+ selected: usize, |
|
| 10 |
+ loading_path: Option<String>, |
|
| 11 |
+ error_path: Option<String>, |
|
| 12 |
+ load_error: Option<String>, |
|
| 13 |
+} |
|
| 14 |
+ |
|
| 15 |
+impl AppSelection {
|
|
| 16 |
+ pub(crate) fn from_preferences(preferences: &AppPreferences) -> Self {
|
|
| 17 |
+ let mut selection = Self::default(); |
|
| 18 |
+ let Some(path) = preferences.path.as_deref() else {
|
|
| 19 |
+ return selection; |
|
| 20 |
+ }; |
|
| 21 |
+ |
|
| 22 |
+ if !is_ipa_path(Path::new(path)) {
|
|
| 23 |
+ selection.fail( |
|
| 24 |
+ path.to_string(), |
|
| 25 |
+ format!("Selected app path is not an IPA archive: {path}"),
|
|
| 26 |
+ ); |
|
| 27 |
+ return selection; |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ match load_ipa(PathBuf::from(path), Vec::new()) {
|
|
| 31 |
+ Ok(mut app) => {
|
|
| 32 |
+ apply_app_overrides(&mut app, &preferences.overrides); |
|
| 33 |
+ selection.push_loaded(app); |
|
| 34 |
+ } |
|
| 35 |
+ Err(error) => {
|
|
| 36 |
+ eprintln!("{error}");
|
|
| 37 |
+ selection.fail(path.to_string(), error); |
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ selection |
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ pub(crate) fn selected(&self) -> Option<&AppOption> {
|
|
| 45 |
+ self.apps.get(self.selected) |
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ pub(crate) fn selected_index(&self) -> usize {
|
|
| 49 |
+ self.selected |
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ pub(crate) fn selected_path_for_preferences(&self) -> Option<String> {
|
|
| 53 |
+ self.selected() |
|
| 54 |
+ .map(|app| app.path.to_string()) |
|
| 55 |
+ .or_else(|| self.error_path.clone()) |
|
| 56 |
+ .or_else(|| self.loading_path.clone()) |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ pub(crate) fn is_loading(&self) -> bool {
|
|
| 60 |
+ self.loading_path.is_some() |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ pub(crate) fn loading_path(&self) -> Option<&str> {
|
|
| 64 |
+ self.loading_path.as_deref() |
|
| 65 |
+ } |
|
| 66 |
+ |
|
| 67 |
+ pub(crate) fn error_path(&self) -> Option<&str> {
|
|
| 68 |
+ self.error_path.as_deref() |
|
| 69 |
+ } |
|
| 70 |
+ |
|
| 71 |
+ pub(crate) fn load_error(&self) -> Option<&str> {
|
|
| 72 |
+ self.load_error.as_deref() |
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ pub(crate) fn replace(&mut self, index: usize, app: AppOption) -> bool {
|
|
| 76 |
+ let Some(existing) = self.apps.get_mut(index) else {
|
|
| 77 |
+ return false; |
|
| 78 |
+ }; |
|
| 79 |
+ *existing = app; |
|
| 80 |
+ true |
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ pub(crate) fn select_cached_path(&mut self, path: &str) -> bool {
|
|
| 84 |
+ let Some(index) = self.apps.iter().position(|app| app.path.as_ref() == path) else {
|
|
| 85 |
+ return false; |
|
| 86 |
+ }; |
|
| 87 |
+ |
|
| 88 |
+ self.selected = index; |
|
| 89 |
+ self.clear_transient_state(); |
|
| 90 |
+ true |
|
| 91 |
+ } |
|
| 92 |
+ |
|
| 93 |
+ pub(crate) fn begin_loading(&mut self, path: String) {
|
|
| 94 |
+ self.loading_path = Some(path); |
|
| 95 |
+ self.error_path = None; |
|
| 96 |
+ self.load_error = None; |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ pub(crate) fn finish_loading(&mut self, path: &Path, result: Result<AppOption, String>) {
|
|
| 100 |
+ match result {
|
|
| 101 |
+ Ok(app) => self.push_loaded(app), |
|
| 102 |
+ Err(error) => {
|
|
| 103 |
+ eprintln!("{error}");
|
|
| 104 |
+ self.fail(path.to_string_lossy().to_string(), error); |
|
| 105 |
+ } |
|
| 106 |
+ } |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ pub(crate) fn fail(&mut self, path: String, error: String) {
|
|
| 110 |
+ self.selected = self.apps.len(); |
|
| 111 |
+ self.loading_path = None; |
|
| 112 |
+ self.error_path = Some(path); |
|
| 113 |
+ self.load_error = Some(error); |
|
| 114 |
+ } |
|
| 115 |
+ |
|
| 116 |
+ fn push_loaded(&mut self, app: AppOption) {
|
|
| 117 |
+ self.apps.push(app); |
|
| 118 |
+ self.selected = self.apps.len() - 1; |
|
| 119 |
+ self.clear_transient_state(); |
|
| 120 |
+ } |
|
| 121 |
+ |
|
| 122 |
+ fn clear_transient_state(&mut self) {
|
|
| 123 |
+ self.loading_path = None; |
|
| 124 |
+ self.error_path = None; |
|
| 125 |
+ self.load_error = None; |
|
| 126 |
+ } |
|
| 127 |
+} |
|
| 128 |
+ |
|
| 129 |
+pub(crate) fn load_ipa(path: PathBuf, patches: Vec<PatchOption>) -> Result<AppOption, String> {
|
|
| 130 |
+ read_ipa(&path, patches) |
|
| 131 |
+} |
|
| 132 |
+ |
|
| 133 |
+pub(crate) fn is_ipa_path(path: &Path) -> bool {
|
|
| 134 |
+ path.extension() |
|
| 135 |
+ .and_then(|extension| extension.to_str()) |
|
| 136 |
+ .is_some_and(|extension| extension.eq_ignore_ascii_case("ipa"))
|
|
| 137 |
+} |
| ... | ... |
@@ -16,8 +16,13 @@ impl AssetSource for Assets {
|
| 16 | 16 |
"icons/plus.svg" => include_bytes!("../assets/icons/plus.svg").as_slice(),
|
| 17 | 17 |
"icons/minus.svg" => include_bytes!("../assets/icons/minus.svg").as_slice(),
|
| 18 | 18 |
"icons/pencil.svg" => include_bytes!("../assets/icons/pencil.svg").as_slice(),
|
| 19 |
+ "icons/check.svg" => include_bytes!("../assets/icons/check.svg").as_slice(),
|
|
| 20 |
+ "icons/x.svg" => include_bytes!("../assets/icons/x.svg").as_slice(),
|
|
| 19 | 21 |
"icons/refresh-cw.svg" => include_bytes!("../assets/icons/refresh-cw.svg").as_slice(),
|
| 22 |
+ "icons/rotate-ccw.svg" => include_bytes!("../assets/icons/rotate-ccw.svg").as_slice(),
|
|
| 20 | 23 |
"icons/arrow-right.svg" => include_bytes!("../assets/icons/arrow-right.svg").as_slice(),
|
| 24 |
+ "icons/log-out.svg" => include_bytes!("../assets/icons/log-out.svg").as_slice(),
|
|
| 25 |
+ "icons/folder-open.svg" => include_bytes!("../assets/icons/folder-open.svg").as_slice(),
|
|
| 21 | 26 |
_ => return Ok(None), |
| 22 | 27 |
}; |
| 23 | 28 |
|
| ... | ... |
@@ -35,8 +40,13 @@ impl AssetSource for Assets {
|
| 35 | 40 |
"plus.svg".into(), |
| 36 | 41 |
"minus.svg".into(), |
| 37 | 42 |
"pencil.svg".into(), |
| 43 |
+ "check.svg".into(), |
|
| 44 |
+ "x.svg".into(), |
|
| 38 | 45 |
"refresh-cw.svg".into(), |
| 46 |
+ "rotate-ccw.svg".into(), |
|
| 39 | 47 |
"arrow-right.svg".into(), |
| 48 |
+ "log-out.svg".into(), |
|
| 49 |
+ "folder-open.svg".into(), |
|
| 40 | 50 |
]) |
| 41 | 51 |
} else {
|
| 42 | 52 |
Ok(Vec::new()) |
| ... | ... |
@@ -4,8 +4,8 @@ pub(crate) const SETTINGS_WINDOW_WIDTH: f32 = 520.; |
| 4 | 4 |
pub(crate) const TEAM_SETTINGS_WINDOW_HEIGHT: f32 = 560.; |
| 5 | 5 |
pub(crate) const APP_SETTINGS_WINDOW_WIDTH: f32 = 520.; |
| 6 | 6 |
pub(crate) const APP_SETTINGS_WINDOW_HEIGHT: f32 = 560.; |
| 7 |
-pub(crate) const ADI_SETTINGS_WINDOW_WIDTH: f32 = 500.; |
|
| 8 |
-pub(crate) const ADI_SETTINGS_WINDOW_HEIGHT: f32 = 500.; |
|
| 7 |
+pub(crate) const ADI_SETTINGS_WINDOW_WIDTH: f32 = 560.; |
|
| 8 |
+pub(crate) const ADI_SETTINGS_WINDOW_HEIGHT: f32 = 520.; |
|
| 9 | 9 |
pub(crate) const ADI_BACKEND_POPOVER_WIDTH: f32 = 420.; |
| 10 | 10 |
pub(crate) const ADI_BACKEND_POPOVER_MAX_HEIGHT: f32 = 240.; |
| 11 | 11 |
pub(crate) const ADI_BACKEND_COMBO_HEIGHT: f32 = 64.; |
| ... | ... |
@@ -1,189 +1,13 @@ |
| 1 |
-use crate::models::{
|
|
| 2 |
- AccountOption, AdiBackendOption, AppIdOption, AppOption, DeviceOption, MachineIdentity, |
|
| 3 |
- PatchOption, TeamOption, |
|
| 4 |
-}; |
|
| 5 |
-use std::env; |
|
| 1 |
+use crate::developer_accounts::load_account_options; |
|
| 2 |
+use crate::models::{AccountOption, MachineIdentity};
|
|
| 6 | 3 |
|
| 7 |
-pub(crate) fn sample_accounts() -> Vec<AccountOption> {
|
|
| 8 |
- let env_apple_id = env::var("APPLE_ID").unwrap_or_else(|_| "Add Apple ID".to_string());
|
|
| 9 |
- let env_detail = if env_apple_id == "Add Apple ID" {
|
|
| 10 |
- "No saved credentials".to_string() |
|
| 11 |
- } else {
|
|
| 12 |
- "Loaded from APPLE_ID".to_string() |
|
| 13 |
- }; |
|
| 14 |
- |
|
| 15 |
- vec![ |
|
| 16 |
- AccountOption {
|
|
| 17 |
- label: "Primary Apple ID".into(), |
|
| 18 |
- apple_id: env_apple_id.into(), |
|
| 19 |
- detail: env_detail.into(), |
|
| 20 |
- status: "Available".into(), |
|
| 21 |
- teams: vec![ |
|
| 22 |
- TeamOption {
|
|
| 23 |
- name: "Personal Team".into(), |
|
| 24 |
- identifier: "LOCAL-PERSONAL".into(), |
|
| 25 |
- role: "Free".into(), |
|
| 26 |
- app_ids: sample_app_ids("LOCAL-PERSONAL"),
|
|
| 27 |
- }, |
|
| 28 |
- TeamOption {
|
|
| 29 |
- name: "Developer Program".into(), |
|
| 30 |
- identifier: "TEAMID1234".into(), |
|
| 31 |
- role: "Paid".into(), |
|
| 32 |
- app_ids: sample_app_ids("TEAMID1234"),
|
|
| 33 |
- }, |
|
| 34 |
- ], |
|
| 35 |
- }, |
|
| 36 |
- AccountOption {
|
|
| 37 |
- label: "Work Apple ID".into(), |
|
| 38 |
- apple_id: "developer@example.com".into(), |
|
| 39 |
- detail: "Example account placeholder".into(), |
|
| 40 |
- status: "Demo".into(), |
|
| 41 |
- teams: vec![ |
|
| 42 |
- TeamOption {
|
|
| 43 |
- name: "Example Apps LLC".into(), |
|
| 44 |
- identifier: "EXAMPL3APP".into(), |
|
| 45 |
- role: "Admin".into(), |
|
| 46 |
- app_ids: sample_app_ids("EXAMPL3APP"),
|
|
| 47 |
- }, |
|
| 48 |
- TeamOption {
|
|
| 49 |
- name: "Client Signing Team".into(), |
|
| 50 |
- identifier: "CL13NTTEAM".into(), |
|
| 51 |
- role: "Member".into(), |
|
| 52 |
- app_ids: sample_app_ids("CL13NTTEAM"),
|
|
| 53 |
- }, |
|
| 54 |
- ], |
|
| 55 |
- }, |
|
| 56 |
- ] |
|
| 57 |
-} |
|
| 58 |
- |
|
| 59 |
-fn sample_app_ids(team_id: &str) -> Vec<AppIdOption> {
|
|
| 60 |
- vec![ |
|
| 61 |
- AppIdOption {
|
|
| 62 |
- name: "Sample App".into(), |
|
| 63 |
- identifier: format!("{team_id}.com.example.sample").into(),
|
|
| 64 |
- kind: "Explicit App ID".into(), |
|
| 65 |
- }, |
|
| 66 |
- AppIdOption {
|
|
| 67 |
- name: "Wildcard Development".into(), |
|
| 68 |
- identifier: format!("{team_id}.*").into(),
|
|
| 69 |
- kind: "Wildcard App ID".into(), |
|
| 70 |
- }, |
|
| 71 |
- ] |
|
| 4 |
+pub(crate) fn load_accounts() -> Vec<AccountOption> {
|
|
| 5 |
+ load_account_options().unwrap_or_else(|error| {
|
|
| 6 |
+ eprintln!("{error}");
|
|
| 7 |
+ Vec::new() |
|
| 8 |
+ }) |
|
| 72 | 9 |
} |
| 73 | 10 |
|
| 74 |
-pub(crate) fn sample_apps() -> Vec<AppOption> {
|
|
| 75 |
- let ipa_path = env::var("IPA_PATH").unwrap_or_else(|_| "/Applications/Sample.ipa".to_string());
|
|
| 76 |
- let bundle_id = env::var("BUNDLE_ID").unwrap_or_else(|_| "com.example.sample".to_string());
|
|
| 77 |
- |
|
| 78 |
- vec![ |
|
| 79 |
- AppOption {
|
|
| 80 |
- name: "Sample IPA".into(), |
|
| 81 |
- bundle_id: bundle_id.into(), |
|
| 82 |
- version: "1.4.2".into(), |
|
| 83 |
- build: "82".into(), |
|
| 84 |
- path: ipa_path.into(), |
|
| 85 |
- patches: vec![ |
|
| 86 |
- PatchOption {
|
|
| 87 |
- name: "Preserve Bundle ID".into(), |
|
| 88 |
- detail: "Do not rewrite CFBundleIdentifier.".into(), |
|
| 89 |
- }, |
|
| 90 |
- PatchOption {
|
|
| 91 |
- name: "Enable Documents".into(), |
|
| 92 |
- detail: "Expose the app documents directory in Files.".into(), |
|
| 93 |
- }, |
|
| 94 |
- PatchOption {
|
|
| 95 |
- name: "Strip Watch Extension".into(), |
|
| 96 |
- detail: "Remove watchOS payloads before signing.".into(), |
|
| 97 |
- }, |
|
| 98 |
- ], |
|
| 99 |
- }, |
|
| 100 |
- AppOption {
|
|
| 101 |
- name: "Debug Build".into(), |
|
| 102 |
- bundle_id: "com.example.debug".into(), |
|
| 103 |
- version: "0.9.0".into(), |
|
| 104 |
- build: "dev-17".into(), |
|
| 105 |
- path: "/tmp/debug-build.ipa".into(), |
|
| 106 |
- patches: vec![ |
|
| 107 |
- PatchOption {
|
|
| 108 |
- name: "Inject Get-Task-Allow".into(), |
|
| 109 |
- detail: "Allow debugger attachment for development.".into(), |
|
| 110 |
- }, |
|
| 111 |
- PatchOption {
|
|
| 112 |
- name: "Remove Beta Expiry".into(), |
|
| 113 |
- detail: "Patch known testflight-style expiry metadata.".into(), |
|
| 114 |
- }, |
|
| 115 |
- ], |
|
| 116 |
- }, |
|
| 117 |
- ] |
|
| 118 |
-} |
|
| 119 |
- |
|
| 120 |
-pub(crate) fn sample_devices() -> Vec<DeviceOption> {
|
|
| 121 |
- let udid = env::var("DEVICE_UDID").unwrap_or_else(|_| "00008110-001234DEADBEEF01".to_string());
|
|
| 122 |
- |
|
| 123 |
- vec![ |
|
| 124 |
- DeviceOption {
|
|
| 125 |
- name: "Connected iPhone".into(), |
|
| 126 |
- model: "iPhone16,2".into(), |
|
| 127 |
- os: "iOS 18.6".into(), |
|
| 128 |
- udid: udid.into(), |
|
| 129 |
- connection: "USB".into(), |
|
| 130 |
- }, |
|
| 131 |
- DeviceOption {
|
|
| 132 |
- name: "Test iPad".into(), |
|
| 133 |
- model: "iPad14,3".into(), |
|
| 134 |
- os: "iPadOS 18.5".into(), |
|
| 135 |
- udid: "00008103-00A1B2C3D4E5F607".into(), |
|
| 136 |
- connection: "Wi-Fi".into(), |
|
| 137 |
- }, |
|
| 138 |
- ] |
|
| 139 |
-} |
|
| 140 |
- |
|
| 141 |
-pub(crate) fn sample_adi_backends() -> Vec<AdiBackendOption> {
|
|
| 142 |
- vec![ |
|
| 143 |
- AdiBackendOption {
|
|
| 144 |
- name: "Native ADI".into(), |
|
| 145 |
- detail: "Use the platform ADI provider for this machine.".into(), |
|
| 146 |
- status: "Ready".into(), |
|
| 147 |
- information: "Uses the local machine identity and platform services for ADI requests." |
|
| 148 |
- .into(), |
|
| 149 |
- editable_identity: false, |
|
| 150 |
- repair_action: None, |
|
| 151 |
- }, |
|
| 152 |
- AdiBackendOption {
|
|
| 153 |
- name: "Provisioned Files".into(), |
|
| 154 |
- detail: "Use a portable ADI identity stored on disk.".into(), |
|
| 155 |
- status: "Needs setup".into(), |
|
| 156 |
- information: |
|
| 157 |
- "Loads ADI provisioning files from the selected profile directory when available." |
|
| 158 |
- .into(), |
|
| 159 |
- editable_identity: true, |
|
| 160 |
- repair_action: Some("Fix Provisioning".into()),
|
|
| 161 |
- }, |
|
| 162 |
- AdiBackendOption {
|
|
| 163 |
- name: "Remote Proxy".into(), |
|
| 164 |
- detail: "Forward ADI requests to a configured service.".into(), |
|
| 165 |
- status: "Offline".into(), |
|
| 166 |
- information: |
|
| 167 |
- "Connects to an external ADI service and uses its reported machine identity.".into(), |
|
| 168 |
- editable_identity: false, |
|
| 169 |
- repair_action: Some("Reconnect".into()),
|
|
| 170 |
- }, |
|
| 171 |
- ] |
|
| 172 |
-} |
|
| 173 |
- |
|
| 174 |
-pub(crate) fn sample_machine_identity() -> MachineIdentity {
|
|
| 175 |
- let machine_name = env::var("HOSTNAME")
|
|
| 176 |
- .or_else(|_| env::var("COMPUTERNAME"))
|
|
| 177 |
- .unwrap_or_else(|_| "Local Machine".to_string()); |
|
| 178 |
- let os_name = env::consts::OS.to_string(); |
|
| 179 |
- let os_version = env::var("OS_VERSION").unwrap_or_else(|_| "Unknown".to_string());
|
|
| 180 |
- let machine_id = env::var("MACHINE_ID")
|
|
| 181 |
- .unwrap_or_else(|_| "A8B31C86-359B-4D95-8950-BA5DD8FFC46F".to_string()); |
|
| 182 |
- |
|
| 183 |
- MachineIdentity {
|
|
| 184 |
- machine_name: machine_name.into(), |
|
| 185 |
- os_name: os_name.into(), |
|
| 186 |
- os_version: os_version.into(), |
|
| 187 |
- machine_id: machine_id.into(), |
|
| 188 |
- } |
|
| 11 |
+pub(crate) fn load_machine_identity() -> MachineIdentity {
|
|
| 12 |
+ crate::system_identity::machine_identity() |
|
| 189 | 13 |
} |
| ... | ... |
@@ -0,0 +1,405 @@ |
| 1 |
+use crate::models::{AccountOption, AppIdOption, TeamOption};
|
|
| 2 |
+use crate::paths::app_data_dir; |
|
| 3 |
+use gpui::SharedString; |
|
| 4 |
+use serde::{Deserialize, Serialize};
|
|
| 5 |
+use std::fs; |
|
| 6 |
+use std::io::ErrorKind; |
|
| 7 |
+use std::path::PathBuf; |
|
| 8 |
+use uuid::Uuid; |
|
| 9 |
+ |
|
| 10 |
+const ACCOUNTS_DIR: &str = "accounts"; |
|
| 11 |
+const ACCOUNT_INDEX_FILE: &str = "index.toml"; |
|
| 12 |
+const CACHE_VERSION: u32 = 1; |
|
| 13 |
+const KEYRING_SERVICE: &str = "com.Dadoum.Super-Sideloader"; |
|
| 14 |
+ |
|
| 15 |
+#[derive(Clone, Debug, Default, Deserialize, Serialize)] |
|
| 16 |
+#[serde(default)] |
|
| 17 |
+pub(crate) struct DeveloperAccountIndex {
|
|
| 18 |
+ pub(crate) version: u32, |
|
| 19 |
+ pub(crate) accounts: Vec<DeveloperAccountIndexEntry>, |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+#[derive(Clone, Debug, Deserialize, Serialize)] |
|
| 23 |
+pub(crate) struct DeveloperAccountIndexEntry {
|
|
| 24 |
+ pub(crate) id: String, |
|
| 25 |
+ pub(crate) email: String, |
|
| 26 |
+ pub(crate) cache_file: String, |
|
| 27 |
+ #[serde(default, skip_serializing_if = "Option::is_none")] |
|
| 28 |
+ pub(crate) profile_name: Option<String>, |
|
| 29 |
+ #[serde(default, skip_serializing_if = "Option::is_none")] |
|
| 30 |
+ pub(crate) token_expires_at: Option<String>, |
|
| 31 |
+ #[serde(default, skip_serializing_if = "Option::is_none")] |
|
| 32 |
+ pub(crate) last_refreshed_at: Option<String>, |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+#[derive(Clone, Debug, Deserialize, Serialize)] |
|
| 36 |
+#[serde(default)] |
|
| 37 |
+pub(crate) struct DeveloperAccountCache {
|
|
| 38 |
+ pub(crate) version: u32, |
|
| 39 |
+ pub(crate) id: String, |
|
| 40 |
+ pub(crate) email: String, |
|
| 41 |
+ pub(crate) profile_name: Option<String>, |
|
| 42 |
+ pub(crate) token_expires_at: Option<String>, |
|
| 43 |
+ pub(crate) last_refreshed_at: Option<String>, |
|
| 44 |
+ pub(crate) teams: Vec<CachedDeveloperTeam>, |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+#[derive(Clone, Debug, Deserialize, Serialize)] |
|
| 48 |
+pub(crate) struct CachedDeveloperTeam {
|
|
| 49 |
+ pub(crate) id: String, |
|
| 50 |
+ pub(crate) name: String, |
|
| 51 |
+ pub(crate) role: String, |
|
| 52 |
+ #[serde(default)] |
|
| 53 |
+ pub(crate) app_ids: Vec<CachedAppId>, |
|
| 54 |
+} |
|
| 55 |
+ |
|
| 56 |
+#[derive(Clone, Debug, Deserialize, Serialize)] |
|
| 57 |
+pub(crate) struct CachedAppId {
|
|
| 58 |
+ pub(crate) id: String, |
|
| 59 |
+ pub(crate) name: String, |
|
| 60 |
+ pub(crate) kind: String, |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+impl Default for DeveloperAccountCache {
|
|
| 64 |
+ fn default() -> Self {
|
|
| 65 |
+ Self {
|
|
| 66 |
+ version: CACHE_VERSION, |
|
| 67 |
+ id: new_account_id(), |
|
| 68 |
+ email: String::new(), |
|
| 69 |
+ profile_name: None, |
|
| 70 |
+ token_expires_at: None, |
|
| 71 |
+ last_refreshed_at: None, |
|
| 72 |
+ teams: Vec::new(), |
|
| 73 |
+ } |
|
| 74 |
+ } |
|
| 75 |
+} |
|
| 76 |
+ |
|
| 77 |
+impl DeveloperAccountCache {
|
|
| 78 |
+ pub(crate) fn ensure_id(&mut self) -> &str {
|
|
| 79 |
+ if self.id.is_empty() {
|
|
| 80 |
+ self.id = new_account_id(); |
|
| 81 |
+ } |
|
| 82 |
+ &self.id |
|
| 83 |
+ } |
|
| 84 |
+} |
|
| 85 |
+ |
|
| 86 |
+impl From<&DeveloperAccountCache> for DeveloperAccountIndexEntry {
|
|
| 87 |
+ fn from(account: &DeveloperAccountCache) -> Self {
|
|
| 88 |
+ Self {
|
|
| 89 |
+ id: account.id.clone(), |
|
| 90 |
+ email: account.email.clone(), |
|
| 91 |
+ cache_file: account_cache_file_name(&account.id), |
|
| 92 |
+ profile_name: account.profile_name.clone(), |
|
| 93 |
+ token_expires_at: account.token_expires_at.clone(), |
|
| 94 |
+ last_refreshed_at: account.last_refreshed_at.clone(), |
|
| 95 |
+ } |
|
| 96 |
+ } |
|
| 97 |
+} |
|
| 98 |
+ |
|
| 99 |
+impl From<DeveloperAccountCache> for AccountOption {
|
|
| 100 |
+ fn from(account: DeveloperAccountCache) -> Self {
|
|
| 101 |
+ let profile_name = account |
|
| 102 |
+ .profile_name |
|
| 103 |
+ .unwrap_or_else(|| account.email.clone()); |
|
| 104 |
+ let status = account |
|
| 105 |
+ .token_expires_at |
|
| 106 |
+ .map(|expires_at| format!("Token expires {expires_at}"))
|
|
| 107 |
+ .unwrap_or_else(|| "Cached".to_string()); |
|
| 108 |
+ let detail = account |
|
| 109 |
+ .last_refreshed_at |
|
| 110 |
+ .map(|refreshed_at| format!("Last refreshed {refreshed_at}"))
|
|
| 111 |
+ .unwrap_or_else(|| "Loaded from account cache".to_string()); |
|
| 112 |
+ |
|
| 113 |
+ Self {
|
|
| 114 |
+ id: account.id.into(), |
|
| 115 |
+ label: profile_name.into(), |
|
| 116 |
+ apple_id: account.email.into(), |
|
| 117 |
+ detail: detail.into(), |
|
| 118 |
+ status: status.into(), |
|
| 119 |
+ teams: account.teams.into_iter().map(TeamOption::from).collect(), |
|
| 120 |
+ } |
|
| 121 |
+ } |
|
| 122 |
+} |
|
| 123 |
+ |
|
| 124 |
+impl From<CachedDeveloperTeam> for TeamOption {
|
|
| 125 |
+ fn from(team: CachedDeveloperTeam) -> Self {
|
|
| 126 |
+ Self {
|
|
| 127 |
+ name: team.name.into(), |
|
| 128 |
+ identifier: team.id.into(), |
|
| 129 |
+ role: team.role.into(), |
|
| 130 |
+ app_ids: team.app_ids.into_iter().map(AppIdOption::from).collect(), |
|
| 131 |
+ } |
|
| 132 |
+ } |
|
| 133 |
+} |
|
| 134 |
+ |
|
| 135 |
+impl From<CachedAppId> for AppIdOption {
|
|
| 136 |
+ fn from(app_id: CachedAppId) -> Self {
|
|
| 137 |
+ Self {
|
|
| 138 |
+ name: app_id.name.into(), |
|
| 139 |
+ identifier: app_id.id.into(), |
|
| 140 |
+ kind: app_id.kind.into(), |
|
| 141 |
+ } |
|
| 142 |
+ } |
|
| 143 |
+} |
|
| 144 |
+ |
|
| 145 |
+pub(crate) fn load_account_options() -> Result<Vec<AccountOption>, String> {
|
|
| 146 |
+ load_cached_accounts().map(|accounts| accounts.into_iter().map(AccountOption::from).collect()) |
|
| 147 |
+} |
|
| 148 |
+ |
|
| 149 |
+pub(crate) fn load_cached_accounts() -> Result<Vec<DeveloperAccountCache>, String> {
|
|
| 150 |
+ let index = load_account_index()?; |
|
| 151 |
+ let mut accounts = Vec::with_capacity(index.accounts.len()); |
|
| 152 |
+ |
|
| 153 |
+ for entry in index.accounts {
|
|
| 154 |
+ match load_account_cache_file(&entry.cache_file) {
|
|
| 155 |
+ Ok(account) => accounts.push(account), |
|
| 156 |
+ Err(error) => eprintln!("{error}"),
|
|
| 157 |
+ } |
|
| 158 |
+ } |
|
| 159 |
+ |
|
| 160 |
+ Ok(accounts) |
|
| 161 |
+} |
|
| 162 |
+ |
|
| 163 |
+#[allow(dead_code)] |
|
| 164 |
+pub(crate) fn save_account_cache(account: &DeveloperAccountCache) -> Result<String, String> {
|
|
| 165 |
+ let mut account = account.clone(); |
|
| 166 |
+ account.ensure_id(); |
|
| 167 |
+ account.version = CACHE_VERSION; |
|
| 168 |
+ let account_id = account.id.clone(); |
|
| 169 |
+ |
|
| 170 |
+ let cache_path = account_cache_path(&account.id)?; |
|
| 171 |
+ if let Some(parent) = cache_path.parent() {
|
|
| 172 |
+ fs::create_dir_all(parent).map_err(|error| {
|
|
| 173 |
+ format!( |
|
| 174 |
+ "Failed to create account cache folder at {}: {error}",
|
|
| 175 |
+ parent.display() |
|
| 176 |
+ ) |
|
| 177 |
+ })?; |
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ let contents = toml::to_string_pretty(&account) |
|
| 181 |
+ .map_err(|error| format!("Failed to encode account cache: {error}"))?;
|
|
| 182 |
+ fs::write(&cache_path, contents).map_err(|error| {
|
|
| 183 |
+ format!( |
|
| 184 |
+ "Failed to write account cache at {}: {error}",
|
|
| 185 |
+ cache_path.display() |
|
| 186 |
+ ) |
|
| 187 |
+ })?; |
|
| 188 |
+ |
|
| 189 |
+ let mut index = load_account_index()?; |
|
| 190 |
+ let entry = DeveloperAccountIndexEntry::from(&account); |
|
| 191 |
+ if let Some(existing) = index |
|
| 192 |
+ .accounts |
|
| 193 |
+ .iter_mut() |
|
| 194 |
+ .find(|existing| existing.id == account.id) |
|
| 195 |
+ {
|
|
| 196 |
+ *existing = entry; |
|
| 197 |
+ } else {
|
|
| 198 |
+ index.accounts.push(entry); |
|
| 199 |
+ } |
|
| 200 |
+ save_account_index(&index)?; |
|
| 201 |
+ Ok(account_id) |
|
| 202 |
+} |
|
| 203 |
+ |
|
| 204 |
+#[allow(dead_code)] |
|
| 205 |
+pub(crate) fn delete_account_cache(account_id: &str) -> Result<(), String> {
|
|
| 206 |
+ let cache_path = account_cache_path(account_id)?; |
|
| 207 |
+ match fs::remove_file(&cache_path) {
|
|
| 208 |
+ Ok(()) => {}
|
|
| 209 |
+ Err(error) if error.kind() == ErrorKind::NotFound => {}
|
|
| 210 |
+ Err(error) => {
|
|
| 211 |
+ return Err(format!( |
|
| 212 |
+ "Failed to remove account cache at {}: {error}",
|
|
| 213 |
+ cache_path.display() |
|
| 214 |
+ )); |
|
| 215 |
+ } |
|
| 216 |
+ } |
|
| 217 |
+ |
|
| 218 |
+ let mut index = load_account_index()?; |
|
| 219 |
+ index.accounts.retain(|account| account.id != account_id); |
|
| 220 |
+ save_account_index(&index) |
|
| 221 |
+} |
|
| 222 |
+ |
|
| 223 |
+pub(crate) fn new_account_id() -> String {
|
|
| 224 |
+ Uuid::new_v4().to_string() |
|
| 225 |
+} |
|
| 226 |
+ |
|
| 227 |
+#[allow(dead_code)] |
|
| 228 |
+pub(crate) fn keyring_service() -> &'static str {
|
|
| 229 |
+ KEYRING_SERVICE |
|
| 230 |
+} |
|
| 231 |
+ |
|
| 232 |
+#[allow(dead_code)] |
|
| 233 |
+pub(crate) fn keyring_account_name(account_id: &str) -> String {
|
|
| 234 |
+ format!("account-session:{account_id}")
|
|
| 235 |
+} |
|
| 236 |
+ |
|
| 237 |
+#[derive(Clone, Debug)] |
|
| 238 |
+pub(crate) enum MockDeveloperLoginOutcome {
|
|
| 239 |
+ SignedIn(AccountOption), |
|
| 240 |
+ RequiresTwoFactor { detail: SharedString },
|
|
| 241 |
+} |
|
| 242 |
+ |
|
| 243 |
+pub(crate) fn mock_developer_login( |
|
| 244 |
+ email: &str, |
|
| 245 |
+ password: &str, |
|
| 246 |
+) -> Result<MockDeveloperLoginOutcome, String> {
|
|
| 247 |
+ if password.is_empty() {
|
|
| 248 |
+ return Err("Enter the account password.".to_string());
|
|
| 249 |
+ } |
|
| 250 |
+ |
|
| 251 |
+ match normalized_mock_email(email).as_deref() {
|
|
| 252 |
+ Some("a@example.com") => Ok(MockDeveloperLoginOutcome::RequiresTwoFactor {
|
|
| 253 |
+ detail: "Enter the six digit code sent to the trusted device.".into(), |
|
| 254 |
+ }), |
|
| 255 |
+ Some("b@example.com") => Ok(MockDeveloperLoginOutcome::SignedIn(mock_account(
|
|
| 256 |
+ "b@example.com", |
|
| 257 |
+ "Mock Apple ID B", |
|
| 258 |
+ "Paid", |
|
| 259 |
+ ))), |
|
| 260 |
+ Some(_) | None => Err("No mock Apple Account matches that email.".to_string()),
|
|
| 261 |
+ } |
|
| 262 |
+} |
|
| 263 |
+ |
|
| 264 |
+pub(crate) fn mock_developer_login_with_code( |
|
| 265 |
+ email: &str, |
|
| 266 |
+ code: &str, |
|
| 267 |
+) -> Result<AccountOption, String> {
|
|
| 268 |
+ match normalized_mock_email(email).as_deref() {
|
|
| 269 |
+ Some("a@example.com") if code.trim() == "123456" => {
|
|
| 270 |
+ Ok(mock_account("a@example.com", "Mock Apple ID A", "Free"))
|
|
| 271 |
+ } |
|
| 272 |
+ Some("a@example.com") => Err("The verification code is not valid.".to_string()),
|
|
| 273 |
+ _ => Err("This mock account is not waiting for verification.".to_string()),
|
|
| 274 |
+ } |
|
| 275 |
+} |
|
| 276 |
+ |
|
| 277 |
+fn normalized_mock_email(email: &str) -> Option<String> {
|
|
| 278 |
+ let email = email.trim().to_ascii_lowercase(); |
|
| 279 |
+ match email.as_str() {
|
|
| 280 |
+ "a" => Some("a@example.com".to_string()),
|
|
| 281 |
+ "b" => Some("b@example.com".to_string()),
|
|
| 282 |
+ "" => None, |
|
| 283 |
+ _ => Some(email), |
|
| 284 |
+ } |
|
| 285 |
+} |
|
| 286 |
+ |
|
| 287 |
+fn mock_account(email: &str, label: &str, role: &str) -> AccountOption {
|
|
| 288 |
+ let team_id = if role == "Paid" {
|
|
| 289 |
+ "MOCKPAID01" |
|
| 290 |
+ } else {
|
|
| 291 |
+ "MOCKFREE01" |
|
| 292 |
+ }; |
|
| 293 |
+ |
|
| 294 |
+ AccountOption {
|
|
| 295 |
+ id: new_account_id().into(), |
|
| 296 |
+ label: label.into(), |
|
| 297 |
+ apple_id: email.to_string().into(), |
|
| 298 |
+ detail: "Mock login session".into(), |
|
| 299 |
+ status: "Available".into(), |
|
| 300 |
+ teams: vec![TeamOption {
|
|
| 301 |
+ name: "Mock Developer Team".into(), |
|
| 302 |
+ identifier: team_id.into(), |
|
| 303 |
+ role: role.into(), |
|
| 304 |
+ app_ids: vec![ |
|
| 305 |
+ AppIdOption {
|
|
| 306 |
+ name: "Selected App".into(), |
|
| 307 |
+ identifier: format!("{team_id}.com.example.app").into(),
|
|
| 308 |
+ kind: "Explicit App ID".into(), |
|
| 309 |
+ }, |
|
| 310 |
+ AppIdOption {
|
|
| 311 |
+ name: "Wildcard Development".into(), |
|
| 312 |
+ identifier: format!("{team_id}.*").into(),
|
|
| 313 |
+ kind: "Wildcard App ID".into(), |
|
| 314 |
+ }, |
|
| 315 |
+ ], |
|
| 316 |
+ }], |
|
| 317 |
+ } |
|
| 318 |
+} |
|
| 319 |
+ |
|
| 320 |
+fn load_account_index() -> Result<DeveloperAccountIndex, String> {
|
|
| 321 |
+ let path = account_index_path()?; |
|
| 322 |
+ let contents = match fs::read_to_string(&path) {
|
|
| 323 |
+ Ok(contents) => contents, |
|
| 324 |
+ Err(error) if error.kind() == ErrorKind::NotFound => {
|
|
| 325 |
+ return Ok(DeveloperAccountIndex {
|
|
| 326 |
+ version: CACHE_VERSION, |
|
| 327 |
+ accounts: Vec::new(), |
|
| 328 |
+ }); |
|
| 329 |
+ } |
|
| 330 |
+ Err(error) => {
|
|
| 331 |
+ return Err(format!( |
|
| 332 |
+ "Failed to read account index at {}: {error}",
|
|
| 333 |
+ path.display() |
|
| 334 |
+ )); |
|
| 335 |
+ } |
|
| 336 |
+ }; |
|
| 337 |
+ |
|
| 338 |
+ let mut index: DeveloperAccountIndex = toml::from_str(&contents).map_err(|error| {
|
|
| 339 |
+ format!( |
|
| 340 |
+ "Failed to parse account index at {}: {error}",
|
|
| 341 |
+ path.display() |
|
| 342 |
+ ) |
|
| 343 |
+ })?; |
|
| 344 |
+ index.version = CACHE_VERSION; |
|
| 345 |
+ Ok(index) |
|
| 346 |
+} |
|
| 347 |
+ |
|
| 348 |
+fn save_account_index(index: &DeveloperAccountIndex) -> Result<(), String> {
|
|
| 349 |
+ let path = account_index_path()?; |
|
| 350 |
+ if let Some(parent) = path.parent() {
|
|
| 351 |
+ fs::create_dir_all(parent).map_err(|error| {
|
|
| 352 |
+ format!( |
|
| 353 |
+ "Failed to create account index folder at {}: {error}",
|
|
| 354 |
+ parent.display() |
|
| 355 |
+ ) |
|
| 356 |
+ })?; |
|
| 357 |
+ } |
|
| 358 |
+ |
|
| 359 |
+ let mut index = index.clone(); |
|
| 360 |
+ index.version = CACHE_VERSION; |
|
| 361 |
+ let contents = toml::to_string_pretty(&index) |
|
| 362 |
+ .map_err(|error| format!("Failed to encode account index: {error}"))?;
|
|
| 363 |
+ fs::write(&path, contents).map_err(|error| {
|
|
| 364 |
+ format!( |
|
| 365 |
+ "Failed to write account index at {}: {error}",
|
|
| 366 |
+ path.display() |
|
| 367 |
+ ) |
|
| 368 |
+ }) |
|
| 369 |
+} |
|
| 370 |
+ |
|
| 371 |
+fn load_account_cache_file(cache_file: &str) -> Result<DeveloperAccountCache, String> {
|
|
| 372 |
+ let path = accounts_dir()?.join(cache_file); |
|
| 373 |
+ let contents = fs::read_to_string(&path).map_err(|error| {
|
|
| 374 |
+ format!( |
|
| 375 |
+ "Failed to read account cache at {}: {error}",
|
|
| 376 |
+ path.display() |
|
| 377 |
+ ) |
|
| 378 |
+ })?; |
|
| 379 |
+ let mut account: DeveloperAccountCache = toml::from_str(&contents).map_err(|error| {
|
|
| 380 |
+ format!( |
|
| 381 |
+ "Failed to parse account cache at {}: {error}",
|
|
| 382 |
+ path.display() |
|
| 383 |
+ ) |
|
| 384 |
+ })?; |
|
| 385 |
+ account.version = CACHE_VERSION; |
|
| 386 |
+ Ok(account) |
|
| 387 |
+} |
|
| 388 |
+ |
|
| 389 |
+fn account_index_path() -> Result<PathBuf, String> {
|
|
| 390 |
+ Ok(accounts_dir()?.join(ACCOUNT_INDEX_FILE)) |
|
| 391 |
+} |
|
| 392 |
+ |
|
| 393 |
+fn account_cache_path(account_id: &str) -> Result<PathBuf, String> {
|
|
| 394 |
+ Ok(accounts_dir()?.join(account_cache_file_name(account_id))) |
|
| 395 |
+} |
|
| 396 |
+ |
|
| 397 |
+fn accounts_dir() -> Result<PathBuf, String> {
|
|
| 398 |
+ app_data_dir() |
|
| 399 |
+ .map(|path| path.join(ACCOUNTS_DIR)) |
|
| 400 |
+ .ok_or_else(|| "The application data folder is not available.".to_string()) |
|
| 401 |
+} |
|
| 402 |
+ |
|
| 403 |
+fn account_cache_file_name(account_id: &str) -> String {
|
|
| 404 |
+ format!("{}.toml", account_id)
|
|
| 405 |
+} |
| ... | ... |
@@ -0,0 +1,152 @@ |
| 1 |
+use crate::models::DeviceOption; |
|
| 2 |
+use futures::{channel::mpsc::UnboundedSender, StreamExt};
|
|
| 3 |
+use idevice::{
|
|
| 4 |
+ provider::UsbmuxdProvider, |
|
| 5 |
+ services::lockdown::LockdownClient, |
|
| 6 |
+ usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdDevice},
|
|
| 7 |
+ IdeviceService, |
|
| 8 |
+}; |
|
| 9 |
+use plist::Value; |
|
| 10 |
+ |
|
| 11 |
+pub(crate) fn discover_devices() -> Result<Vec<DeviceOption>, String> {
|
|
| 12 |
+ let runtime = tokio::runtime::Builder::new_current_thread() |
|
| 13 |
+ .enable_all() |
|
| 14 |
+ .build() |
|
| 15 |
+ .map_err(|error| format!("Unable to start device runtime: {error}"))?;
|
|
| 16 |
+ |
|
| 17 |
+ runtime.block_on(discover_devices_async()) |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+pub(crate) fn watch_device_changes( |
|
| 21 |
+ sender: UnboundedSender<Result<(), String>>, |
|
| 22 |
+) -> Result<(), String> {
|
|
| 23 |
+ let runtime = tokio::runtime::Builder::new_current_thread() |
|
| 24 |
+ .enable_all() |
|
| 25 |
+ .build() |
|
| 26 |
+ .map_err(|error| format!("Unable to start device runtime: {error}"))?;
|
|
| 27 |
+ |
|
| 28 |
+ runtime.block_on(watch_device_changes_async(sender)) |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+async fn watch_device_changes_async( |
|
| 32 |
+ sender: UnboundedSender<Result<(), String>>, |
|
| 33 |
+) -> Result<(), String> {
|
|
| 34 |
+ let addr = |
|
| 35 |
+ UsbmuxdAddr::from_env_var().map_err(|error| format!("Invalid usbmuxd address: {error}"))?;
|
|
| 36 |
+ let mut usbmuxd = addr |
|
| 37 |
+ .connect(1) |
|
| 38 |
+ .await |
|
| 39 |
+ .map_err(|error| format!("Unable to connect to usbmuxd: {error}"))?;
|
|
| 40 |
+ let mut events = usbmuxd |
|
| 41 |
+ .listen() |
|
| 42 |
+ .await |
|
| 43 |
+ .map_err(|error| format!("Unable to listen for device changes: {error}"))?;
|
|
| 44 |
+ |
|
| 45 |
+ while let Some(event) = events.next().await {
|
|
| 46 |
+ match event {
|
|
| 47 |
+ Ok(_) => {
|
|
| 48 |
+ if sender.unbounded_send(Ok(())).is_err() {
|
|
| 49 |
+ return Ok(()); |
|
| 50 |
+ } |
|
| 51 |
+ } |
|
| 52 |
+ Err(error) => {
|
|
| 53 |
+ let message = format!("Unable to read device change: {error}");
|
|
| 54 |
+ let _ = sender.unbounded_send(Err(message.clone())); |
|
| 55 |
+ return Err(message); |
|
| 56 |
+ } |
|
| 57 |
+ } |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ let message = "Device change listener ended".to_string(); |
|
| 61 |
+ let _ = sender.unbounded_send(Err(message.clone())); |
|
| 62 |
+ Err(message) |
|
| 63 |
+} |
|
| 64 |
+ |
|
| 65 |
+async fn discover_devices_async() -> Result<Vec<DeviceOption>, String> {
|
|
| 66 |
+ let addr = |
|
| 67 |
+ UsbmuxdAddr::from_env_var().map_err(|error| format!("Invalid usbmuxd address: {error}"))?;
|
|
| 68 |
+ let mut usbmuxd = addr |
|
| 69 |
+ .connect(1) |
|
| 70 |
+ .await |
|
| 71 |
+ .map_err(|error| format!("Unable to connect to usbmuxd: {error}"))?;
|
|
| 72 |
+ let devices = usbmuxd |
|
| 73 |
+ .get_devices() |
|
| 74 |
+ .await |
|
| 75 |
+ .map_err(|error| format!("Unable to list devices: {error}"))?;
|
|
| 76 |
+ |
|
| 77 |
+ let mut discovered = Vec::with_capacity(devices.len()); |
|
| 78 |
+ for (index, device) in devices.into_iter().enumerate() {
|
|
| 79 |
+ discovered.push(describe_device(addr.clone(), index as u32 + 2, device).await); |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ Ok(discovered) |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+async fn describe_device(addr: UsbmuxdAddr, tag: u32, device: UsbmuxdDevice) -> DeviceOption {
|
|
| 86 |
+ let udid = device.udid.clone(); |
|
| 87 |
+ let connection = connection_label(&device.connection_type); |
|
| 88 |
+ let provider = UsbmuxdProvider {
|
|
| 89 |
+ addr, |
|
| 90 |
+ tag, |
|
| 91 |
+ udid: device.udid, |
|
| 92 |
+ device_id: device.device_id, |
|
| 93 |
+ label: "super-sideloader".to_string(), |
|
| 94 |
+ }; |
|
| 95 |
+ |
|
| 96 |
+ let mut name = "Connected Device".to_string(); |
|
| 97 |
+ let mut model = "Unknown model".to_string(); |
|
| 98 |
+ let mut os = "Unknown OS".to_string(); |
|
| 99 |
+ |
|
| 100 |
+ if let Ok(mut lockdown) = LockdownClient::connect(&provider).await {
|
|
| 101 |
+ name = lockdown_string(&mut lockdown, "DeviceName") |
|
| 102 |
+ .await |
|
| 103 |
+ .filter(|value| !value.is_empty()) |
|
| 104 |
+ .unwrap_or(name); |
|
| 105 |
+ model = lockdown_string(&mut lockdown, "ProductType") |
|
| 106 |
+ .await |
|
| 107 |
+ .filter(|value| !value.is_empty()) |
|
| 108 |
+ .unwrap_or(model); |
|
| 109 |
+ |
|
| 110 |
+ let version = lockdown_string(&mut lockdown, "ProductVersion").await; |
|
| 111 |
+ let class = lockdown_string(&mut lockdown, "DeviceClass").await; |
|
| 112 |
+ if let Some(version) = version.filter(|value| !value.is_empty()) {
|
|
| 113 |
+ os = format!("{} {version}", os_name(class.as_deref(), &model));
|
|
| 114 |
+ } |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ DeviceOption {
|
|
| 118 |
+ name: name.into(), |
|
| 119 |
+ model: model.into(), |
|
| 120 |
+ os: os.into(), |
|
| 121 |
+ udid: udid.into(), |
|
| 122 |
+ connection: connection.into(), |
|
| 123 |
+ } |
|
| 124 |
+} |
|
| 125 |
+ |
|
| 126 |
+async fn lockdown_string(lockdown: &mut LockdownClient, key: &str) -> Option<String> {
|
|
| 127 |
+ match lockdown.get_value(Some(key), None).await.ok()? {
|
|
| 128 |
+ Value::String(value) => Some(value), |
|
| 129 |
+ _ => None, |
|
| 130 |
+ } |
|
| 131 |
+} |
|
| 132 |
+ |
|
| 133 |
+fn connection_label(connection: &Connection) -> String {
|
|
| 134 |
+ match connection {
|
|
| 135 |
+ Connection::Usb => "USB".to_string(), |
|
| 136 |
+ Connection::Network(_) => "Wi-Fi".to_string(), |
|
| 137 |
+ Connection::Unknown(label) => label.clone(), |
|
| 138 |
+ } |
|
| 139 |
+} |
|
| 140 |
+ |
|
| 141 |
+fn os_name(device_class: Option<&str>, model: &str) -> &'static str {
|
|
| 142 |
+ match device_class {
|
|
| 143 |
+ Some("iPad") => "iPadOS",
|
|
| 144 |
+ Some("AppleTV") => "tvOS",
|
|
| 145 |
+ Some("Watch") => "watchOS",
|
|
| 146 |
+ Some("iPhone") | Some("iPod") => "iOS",
|
|
| 147 |
+ _ if model.starts_with("iPad") => "iPadOS",
|
|
| 148 |
+ _ if model.starts_with("AppleTV") => "tvOS",
|
|
| 149 |
+ _ if model.starts_with("Watch") => "watchOS",
|
|
| 150 |
+ _ => "iOS", |
|
| 151 |
+ } |
|
| 152 |
+} |
| ... | ... |
@@ -0,0 +1,146 @@ |
| 1 |
+use crate::models::DeviceOption; |
|
| 2 |
+use crate::preferences::DevicePreferences; |
|
| 3 |
+ |
|
| 4 |
+#[derive(Debug, Default)] |
|
| 5 |
+pub(crate) struct DeviceSelection {
|
|
| 6 |
+ devices: Vec<DeviceOption>, |
|
| 7 |
+ selected: usize, |
|
| 8 |
+ selected_key: Option<(String, String)>, |
|
| 9 |
+ refreshing: bool, |
|
| 10 |
+ events_watching: bool, |
|
| 11 |
+ refresh_generation: u64, |
|
| 12 |
+ refresh_error: Option<String>, |
|
| 13 |
+} |
|
| 14 |
+ |
|
| 15 |
+impl DeviceSelection {
|
|
| 16 |
+ pub(crate) fn new(preferences: Option<&DevicePreferences>) -> Self {
|
|
| 17 |
+ Self {
|
|
| 18 |
+ selected_key: preferences |
|
| 19 |
+ .map(|device| (device.udid.clone(), device.connection.clone())), |
|
| 20 |
+ ..Self::default() |
|
| 21 |
+ } |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ pub(crate) fn selected(&self) -> Option<&DeviceOption> {
|
|
| 25 |
+ self.devices.get(self.selected) |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ pub(crate) fn selected_index(&self) -> usize {
|
|
| 29 |
+ self.selected |
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ pub(crate) fn len(&self) -> usize {
|
|
| 33 |
+ self.devices.len() |
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ pub(crate) fn is_empty(&self) -> bool {
|
|
| 37 |
+ self.devices.is_empty() |
|
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ pub(crate) fn is_refreshing(&self) -> bool {
|
|
| 41 |
+ self.refreshing |
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ pub(crate) fn refresh_error(&self) -> Option<&str> {
|
|
| 45 |
+ self.refresh_error.as_deref() |
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ pub(crate) fn selected_preferences(&self) -> Option<DevicePreferences> {
|
|
| 49 |
+ self.selected_key |
|
| 50 |
+ .clone() |
|
| 51 |
+ .or_else(|| self.selected().map(device_selection_key)) |
|
| 52 |
+ .map(|(udid, connection)| DevicePreferences { udid, connection })
|
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ pub(crate) fn device(&self, index: usize) -> Option<&DeviceOption> {
|
|
| 56 |
+ self.devices.get(index) |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ pub(crate) fn select(&mut self, index: usize) -> bool {
|
|
| 60 |
+ let Some(device) = self.devices.get(index) else {
|
|
| 61 |
+ return false; |
|
| 62 |
+ }; |
|
| 63 |
+ |
|
| 64 |
+ self.selected = index; |
|
| 65 |
+ self.selected_key = Some(device_selection_key(device)); |
|
| 66 |
+ true |
|
| 67 |
+ } |
|
| 68 |
+ |
|
| 69 |
+ pub(crate) fn begin_refresh(&mut self) -> Option<u64> {
|
|
| 70 |
+ if self.refreshing {
|
|
| 71 |
+ return None; |
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ self.refreshing = true; |
|
| 75 |
+ self.refresh_generation = self.refresh_generation.wrapping_add(1); |
|
| 76 |
+ Some(self.refresh_generation) |
|
| 77 |
+ } |
|
| 78 |
+ |
|
| 79 |
+ pub(crate) fn finish_refresh( |
|
| 80 |
+ &mut self, |
|
| 81 |
+ generation: u64, |
|
| 82 |
+ result: Result<Vec<DeviceOption>, String>, |
|
| 83 |
+ ) {
|
|
| 84 |
+ if generation != self.refresh_generation {
|
|
| 85 |
+ return; |
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ self.refreshing = false; |
|
| 89 |
+ match result {
|
|
| 90 |
+ Ok(devices) => {
|
|
| 91 |
+ self.refresh_error = None; |
|
| 92 |
+ self.replace_devices(devices); |
|
| 93 |
+ } |
|
| 94 |
+ Err(error) => {
|
|
| 95 |
+ self.refresh_error = Some(error); |
|
| 96 |
+ } |
|
| 97 |
+ } |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ pub(crate) fn start_events_watch(&mut self) -> bool {
|
|
| 101 |
+ if self.events_watching {
|
|
| 102 |
+ return false; |
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ self.events_watching = true; |
|
| 106 |
+ true |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ pub(crate) fn finish_events_watch(&mut self) {
|
|
| 110 |
+ self.events_watching = false; |
|
| 111 |
+ } |
|
| 112 |
+ |
|
| 113 |
+ pub(crate) fn note_device_event(&mut self) {
|
|
| 114 |
+ self.refresh_error = None; |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ pub(crate) fn fail_events_watch(&mut self, error: String) {
|
|
| 118 |
+ self.events_watching = false; |
|
| 119 |
+ self.refresh_error = Some(error); |
|
| 120 |
+ } |
|
| 121 |
+ |
|
| 122 |
+ fn replace_devices(&mut self, devices: Vec<DeviceOption>) {
|
|
| 123 |
+ let selected_key = self |
|
| 124 |
+ .selected_key |
|
| 125 |
+ .clone() |
|
| 126 |
+ .or_else(|| self.selected().map(device_selection_key)); |
|
| 127 |
+ self.devices = devices; |
|
| 128 |
+ self.selected = selected_key |
|
| 129 |
+ .and_then(|key| {
|
|
| 130 |
+ self.devices |
|
| 131 |
+ .iter() |
|
| 132 |
+ .position(|device| device_selection_key(device) == key) |
|
| 133 |
+ }) |
|
| 134 |
+ .unwrap_or(0); |
|
| 135 |
+ |
|
| 136 |
+ if self.selected >= self.devices.len() {
|
|
| 137 |
+ self.selected = 0; |
|
| 138 |
+ } |
|
| 139 |
+ |
|
| 140 |
+ self.selected_key = self.selected().map(device_selection_key); |
|
| 141 |
+ } |
|
| 142 |
+} |
|
| 143 |
+ |
|
| 144 |
+pub(crate) fn device_selection_key(device: &DeviceOption) -> (String, String) {
|
|
| 145 |
+ (device.udid.to_string(), device.connection.to_string()) |
|
| 146 |
+} |
| ... | ... |
@@ -0,0 +1,702 @@ |
| 1 |
+use gpui::{
|
|
| 2 |
+ actions, div, fill, point, prelude::*, px, relative, rgb, rgba, size, App, Bounds, |
|
| 3 |
+ ClipboardItem, Context, CursorStyle, Element, ElementId, ElementInputHandler, Entity, |
|
| 4 |
+ EntityInputHandler, FocusHandle, Focusable, GlobalElementId, KeyBinding, LayoutId, MouseButton, |
|
| 5 |
+ MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, ShapedLine, |
|
| 6 |
+ SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, Window, |
|
| 7 |
+}; |
|
| 8 |
+use std::ops::Range; |
|
| 9 |
+ |
|
| 10 |
+const CONTEXT: &str = "EditLine"; |
|
| 11 |
+ |
|
| 12 |
+actions!( |
|
| 13 |
+ edit_line, |
|
| 14 |
+ [ |
|
| 15 |
+ Backspace, |
|
| 16 |
+ Delete, |
|
| 17 |
+ Left, |
|
| 18 |
+ Right, |
|
| 19 |
+ SelectLeft, |
|
| 20 |
+ SelectRight, |
|
| 21 |
+ SelectAll, |
|
| 22 |
+ Home, |
|
| 23 |
+ End, |
|
| 24 |
+ Paste, |
|
| 25 |
+ Cut, |
|
| 26 |
+ Copy, |
|
| 27 |
+ ShowCharacterPalette, |
|
| 28 |
+ ] |
|
| 29 |
+); |
|
| 30 |
+ |
|
| 31 |
+pub(crate) fn init(cx: &mut App) {
|
|
| 32 |
+ cx.bind_keys([ |
|
| 33 |
+ KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
|
|
| 34 |
+ KeyBinding::new("delete", Delete, Some(CONTEXT)),
|
|
| 35 |
+ KeyBinding::new("left", Left, Some(CONTEXT)),
|
|
| 36 |
+ KeyBinding::new("right", Right, Some(CONTEXT)),
|
|
| 37 |
+ KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)),
|
|
| 38 |
+ KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
|
|
| 39 |
+ KeyBinding::new("home", Home, Some(CONTEXT)),
|
|
| 40 |
+ KeyBinding::new("end", End, Some(CONTEXT)),
|
|
| 41 |
+ #[cfg(target_os = "macos")] |
|
| 42 |
+ KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)),
|
|
| 43 |
+ #[cfg(not(target_os = "macos"))] |
|
| 44 |
+ KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)),
|
|
| 45 |
+ #[cfg(target_os = "macos")] |
|
| 46 |
+ KeyBinding::new("cmd-v", Paste, Some(CONTEXT)),
|
|
| 47 |
+ #[cfg(not(target_os = "macos"))] |
|
| 48 |
+ KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)),
|
|
| 49 |
+ #[cfg(target_os = "macos")] |
|
| 50 |
+ KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
|
|
| 51 |
+ #[cfg(not(target_os = "macos"))] |
|
| 52 |
+ KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
|
|
| 53 |
+ #[cfg(target_os = "macos")] |
|
| 54 |
+ KeyBinding::new("cmd-x", Cut, Some(CONTEXT)),
|
|
| 55 |
+ #[cfg(not(target_os = "macos"))] |
|
| 56 |
+ KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)),
|
|
| 57 |
+ #[cfg(target_os = "macos")] |
|
| 58 |
+ KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)),
|
|
| 59 |
+ ]); |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+pub(crate) struct EditLine {
|
|
| 63 |
+ focus_handle: FocusHandle, |
|
| 64 |
+ content: SharedString, |
|
| 65 |
+ selected_range: Range<usize>, |
|
| 66 |
+ selection_reversed: bool, |
|
| 67 |
+ marked_range: Option<Range<usize>>, |
|
| 68 |
+ last_layout: Option<ShapedLine>, |
|
| 69 |
+ last_bounds: Option<Bounds<Pixels>>, |
|
| 70 |
+ scroll_x: Pixels, |
|
| 71 |
+ is_selecting: bool, |
|
| 72 |
+ masked: bool, |
|
| 73 |
+} |
|
| 74 |
+ |
|
| 75 |
+impl EditLine {
|
|
| 76 |
+ pub(crate) fn new(value: impl Into<SharedString>, cx: &mut Context<Self>) -> Self {
|
|
| 77 |
+ let content = value.into(); |
|
| 78 |
+ let end = content.len(); |
|
| 79 |
+ Self {
|
|
| 80 |
+ focus_handle: cx.focus_handle().tab_stop(true), |
|
| 81 |
+ content, |
|
| 82 |
+ selected_range: end..end, |
|
| 83 |
+ selection_reversed: false, |
|
| 84 |
+ marked_range: None, |
|
| 85 |
+ last_layout: None, |
|
| 86 |
+ last_bounds: None, |
|
| 87 |
+ scroll_x: Pixels::ZERO, |
|
| 88 |
+ is_selecting: false, |
|
| 89 |
+ masked: false, |
|
| 90 |
+ } |
|
| 91 |
+ } |
|
| 92 |
+ |
|
| 93 |
+ pub(crate) fn new_password(value: impl Into<SharedString>, cx: &mut Context<Self>) -> Self {
|
|
| 94 |
+ let mut input = Self::new(value, cx); |
|
| 95 |
+ input.masked = true; |
|
| 96 |
+ input |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ pub(crate) fn value(&self) -> String {
|
|
| 100 |
+ self.content.to_string() |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ pub(crate) fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 104 |
+ self.focus_handle.focus(window, cx); |
|
| 105 |
+ } |
|
| 106 |
+ |
|
| 107 |
+ fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
|
|
| 108 |
+ if self.selected_range.is_empty() {
|
|
| 109 |
+ self.move_to(self.previous_boundary(self.cursor_offset()), cx); |
|
| 110 |
+ } else {
|
|
| 111 |
+ self.move_to(self.selected_range.start, cx) |
|
| 112 |
+ } |
|
| 113 |
+ } |
|
| 114 |
+ |
|
| 115 |
+ fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
|
|
| 116 |
+ if self.selected_range.is_empty() {
|
|
| 117 |
+ self.move_to(self.next_boundary(self.selected_range.end), cx); |
|
| 118 |
+ } else {
|
|
| 119 |
+ self.move_to(self.selected_range.end, cx) |
|
| 120 |
+ } |
|
| 121 |
+ } |
|
| 122 |
+ |
|
| 123 |
+ fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
|
|
| 124 |
+ self.select_to(self.previous_boundary(self.cursor_offset()), cx); |
|
| 125 |
+ } |
|
| 126 |
+ |
|
| 127 |
+ fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
|
|
| 128 |
+ self.select_to(self.next_boundary(self.cursor_offset()), cx); |
|
| 129 |
+ } |
|
| 130 |
+ |
|
| 131 |
+ fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
|
|
| 132 |
+ self.move_to(0, cx); |
|
| 133 |
+ self.select_to(self.content.len(), cx) |
|
| 134 |
+ } |
|
| 135 |
+ |
|
| 136 |
+ fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
|
|
| 137 |
+ self.move_to(0, cx); |
|
| 138 |
+ } |
|
| 139 |
+ |
|
| 140 |
+ fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
|
|
| 141 |
+ self.move_to(self.content.len(), cx); |
|
| 142 |
+ } |
|
| 143 |
+ |
|
| 144 |
+ fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 145 |
+ if self.selected_range.is_empty() {
|
|
| 146 |
+ self.select_to(self.previous_boundary(self.cursor_offset()), cx) |
|
| 147 |
+ } |
|
| 148 |
+ self.replace_text_in_range(None, "", window, cx) |
|
| 149 |
+ } |
|
| 150 |
+ |
|
| 151 |
+ fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 152 |
+ if self.selected_range.is_empty() {
|
|
| 153 |
+ self.select_to(self.next_boundary(self.cursor_offset()), cx) |
|
| 154 |
+ } |
|
| 155 |
+ self.replace_text_in_range(None, "", window, cx) |
|
| 156 |
+ } |
|
| 157 |
+ |
|
| 158 |
+ fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 159 |
+ if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
|
|
| 160 |
+ self.replace_text_in_range(None, &text.replace('\n', " "), window, cx);
|
|
| 161 |
+ } |
|
| 162 |
+ } |
|
| 163 |
+ |
|
| 164 |
+ fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
|
|
| 165 |
+ if !self.selected_range.is_empty() {
|
|
| 166 |
+ cx.write_to_clipboard(ClipboardItem::new_string( |
|
| 167 |
+ self.content[self.selected_range.clone()].to_string(), |
|
| 168 |
+ )); |
|
| 169 |
+ } |
|
| 170 |
+ } |
|
| 171 |
+ |
|
| 172 |
+ fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 173 |
+ if !self.selected_range.is_empty() {
|
|
| 174 |
+ cx.write_to_clipboard(ClipboardItem::new_string( |
|
| 175 |
+ self.content[self.selected_range.clone()].to_string(), |
|
| 176 |
+ )); |
|
| 177 |
+ self.replace_text_in_range(None, "", window, cx); |
|
| 178 |
+ } |
|
| 179 |
+ } |
|
| 180 |
+ |
|
| 181 |
+ fn show_character_palette( |
|
| 182 |
+ &mut self, |
|
| 183 |
+ _: &ShowCharacterPalette, |
|
| 184 |
+ window: &mut Window, |
|
| 185 |
+ _: &mut Context<Self>, |
|
| 186 |
+ ) {
|
|
| 187 |
+ window.show_character_palette(); |
|
| 188 |
+ } |
|
| 189 |
+ |
|
| 190 |
+ fn on_mouse_down( |
|
| 191 |
+ &mut self, |
|
| 192 |
+ event: &MouseDownEvent, |
|
| 193 |
+ window: &mut Window, |
|
| 194 |
+ cx: &mut Context<Self>, |
|
| 195 |
+ ) {
|
|
| 196 |
+ self.focus_handle.focus(window, cx); |
|
| 197 |
+ self.is_selecting = true; |
|
| 198 |
+ if event.modifiers.shift {
|
|
| 199 |
+ self.select_to(self.index_for_mouse_position(event.position), cx); |
|
| 200 |
+ } else {
|
|
| 201 |
+ self.move_to(self.index_for_mouse_position(event.position), cx); |
|
| 202 |
+ } |
|
| 203 |
+ } |
|
| 204 |
+ |
|
| 205 |
+ fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, _: &mut Context<Self>) {
|
|
| 206 |
+ self.is_selecting = false; |
|
| 207 |
+ } |
|
| 208 |
+ |
|
| 209 |
+ fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
|
|
| 210 |
+ if self.is_selecting {
|
|
| 211 |
+ self.select_to(self.index_for_mouse_position(event.position), cx); |
|
| 212 |
+ } |
|
| 213 |
+ } |
|
| 214 |
+ |
|
| 215 |
+ fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
|
| 216 |
+ self.selected_range = offset..offset; |
|
| 217 |
+ self.selection_reversed = false; |
|
| 218 |
+ cx.notify(); |
|
| 219 |
+ } |
|
| 220 |
+ |
|
| 221 |
+ fn cursor_offset(&self) -> usize {
|
|
| 222 |
+ if self.selection_reversed {
|
|
| 223 |
+ self.selected_range.start |
|
| 224 |
+ } else {
|
|
| 225 |
+ self.selected_range.end |
|
| 226 |
+ } |
|
| 227 |
+ } |
|
| 228 |
+ |
|
| 229 |
+ fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
|
|
| 230 |
+ if self.content.is_empty() {
|
|
| 231 |
+ return 0; |
|
| 232 |
+ } |
|
| 233 |
+ |
|
| 234 |
+ let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref()) |
|
| 235 |
+ else {
|
|
| 236 |
+ return 0; |
|
| 237 |
+ }; |
|
| 238 |
+ if position.y < bounds.top() {
|
|
| 239 |
+ return 0; |
|
| 240 |
+ } |
|
| 241 |
+ if position.y > bounds.bottom() {
|
|
| 242 |
+ return self.content.len(); |
|
| 243 |
+ } |
|
| 244 |
+ line.closest_index_for_x(position.x - bounds.left() + self.scroll_x) |
|
| 245 |
+ } |
|
| 246 |
+ |
|
| 247 |
+ fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
|
|
| 248 |
+ if self.selection_reversed {
|
|
| 249 |
+ self.selected_range.start = offset |
|
| 250 |
+ } else {
|
|
| 251 |
+ self.selected_range.end = offset |
|
| 252 |
+ }; |
|
| 253 |
+ if self.selected_range.end < self.selected_range.start {
|
|
| 254 |
+ self.selection_reversed = !self.selection_reversed; |
|
| 255 |
+ self.selected_range = self.selected_range.end..self.selected_range.start; |
|
| 256 |
+ } |
|
| 257 |
+ cx.notify(); |
|
| 258 |
+ } |
|
| 259 |
+ |
|
| 260 |
+ fn offset_from_utf16(&self, offset: usize) -> usize {
|
|
| 261 |
+ let mut utf8_offset = 0; |
|
| 262 |
+ let mut utf16_count = 0; |
|
| 263 |
+ |
|
| 264 |
+ for ch in self.content.chars() {
|
|
| 265 |
+ if utf16_count >= offset {
|
|
| 266 |
+ break; |
|
| 267 |
+ } |
|
| 268 |
+ utf16_count += ch.len_utf16(); |
|
| 269 |
+ utf8_offset += ch.len_utf8(); |
|
| 270 |
+ } |
|
| 271 |
+ |
|
| 272 |
+ utf8_offset |
|
| 273 |
+ } |
|
| 274 |
+ |
|
| 275 |
+ fn offset_to_utf16(&self, offset: usize) -> usize {
|
|
| 276 |
+ let mut utf16_offset = 0; |
|
| 277 |
+ let mut utf8_count = 0; |
|
| 278 |
+ |
|
| 279 |
+ for ch in self.content.chars() {
|
|
| 280 |
+ if utf8_count >= offset {
|
|
| 281 |
+ break; |
|
| 282 |
+ } |
|
| 283 |
+ utf8_count += ch.len_utf8(); |
|
| 284 |
+ utf16_offset += ch.len_utf16(); |
|
| 285 |
+ } |
|
| 286 |
+ |
|
| 287 |
+ utf16_offset |
|
| 288 |
+ } |
|
| 289 |
+ |
|
| 290 |
+ fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
|
|
| 291 |
+ self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end) |
|
| 292 |
+ } |
|
| 293 |
+ |
|
| 294 |
+ fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
|
| 295 |
+ self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end) |
|
| 296 |
+ } |
|
| 297 |
+ |
|
| 298 |
+ fn previous_boundary(&self, offset: usize) -> usize {
|
|
| 299 |
+ let offset = snap_to_char_boundary(&self.content, offset); |
|
| 300 |
+ self.content[..offset] |
|
| 301 |
+ .char_indices() |
|
| 302 |
+ .last() |
|
| 303 |
+ .map(|(index, _)| index) |
|
| 304 |
+ .unwrap_or(0) |
|
| 305 |
+ } |
|
| 306 |
+ |
|
| 307 |
+ fn next_boundary(&self, offset: usize) -> usize {
|
|
| 308 |
+ let offset = snap_to_char_boundary(&self.content, offset); |
|
| 309 |
+ self.content[offset..] |
|
| 310 |
+ .char_indices() |
|
| 311 |
+ .nth(1) |
|
| 312 |
+ .map(|(index, _)| offset + index) |
|
| 313 |
+ .unwrap_or(self.content.len()) |
|
| 314 |
+ } |
|
| 315 |
+} |
|
| 316 |
+ |
|
| 317 |
+impl EntityInputHandler for EditLine {
|
|
| 318 |
+ fn text_for_range( |
|
| 319 |
+ &mut self, |
|
| 320 |
+ range_utf16: Range<usize>, |
|
| 321 |
+ actual_range: &mut Option<Range<usize>>, |
|
| 322 |
+ _: &mut Window, |
|
| 323 |
+ _: &mut Context<Self>, |
|
| 324 |
+ ) -> Option<String> {
|
|
| 325 |
+ let range = self.range_from_utf16(&range_utf16); |
|
| 326 |
+ actual_range.replace(self.range_to_utf16(&range)); |
|
| 327 |
+ Some(self.content[range].to_string()) |
|
| 328 |
+ } |
|
| 329 |
+ |
|
| 330 |
+ fn selected_text_range( |
|
| 331 |
+ &mut self, |
|
| 332 |
+ _: bool, |
|
| 333 |
+ _: &mut Window, |
|
| 334 |
+ _: &mut Context<Self>, |
|
| 335 |
+ ) -> Option<UTF16Selection> {
|
|
| 336 |
+ Some(UTF16Selection {
|
|
| 337 |
+ range: self.range_to_utf16(&self.selected_range), |
|
| 338 |
+ reversed: self.selection_reversed, |
|
| 339 |
+ }) |
|
| 340 |
+ } |
|
| 341 |
+ |
|
| 342 |
+ fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> {
|
|
| 343 |
+ self.marked_range |
|
| 344 |
+ .as_ref() |
|
| 345 |
+ .map(|range| self.range_to_utf16(range)) |
|
| 346 |
+ } |
|
| 347 |
+ |
|
| 348 |
+ fn unmark_text(&mut self, _: &mut Window, _: &mut Context<Self>) {
|
|
| 349 |
+ self.marked_range = None; |
|
| 350 |
+ } |
|
| 351 |
+ |
|
| 352 |
+ fn replace_text_in_range( |
|
| 353 |
+ &mut self, |
|
| 354 |
+ range_utf16: Option<Range<usize>>, |
|
| 355 |
+ new_text: &str, |
|
| 356 |
+ _: &mut Window, |
|
| 357 |
+ cx: &mut Context<Self>, |
|
| 358 |
+ ) {
|
|
| 359 |
+ let range = range_utf16 |
|
| 360 |
+ .as_ref() |
|
| 361 |
+ .map(|range_utf16| self.range_from_utf16(range_utf16)) |
|
| 362 |
+ .or(self.marked_range.clone()) |
|
| 363 |
+ .unwrap_or(self.selected_range.clone()); |
|
| 364 |
+ |
|
| 365 |
+ self.content = |
|
| 366 |
+ (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..]) |
|
| 367 |
+ .into(); |
|
| 368 |
+ self.selected_range = range.start + new_text.len()..range.start + new_text.len(); |
|
| 369 |
+ self.selection_reversed = false; |
|
| 370 |
+ self.marked_range.take(); |
|
| 371 |
+ cx.notify(); |
|
| 372 |
+ } |
|
| 373 |
+ |
|
| 374 |
+ fn replace_and_mark_text_in_range( |
|
| 375 |
+ &mut self, |
|
| 376 |
+ range_utf16: Option<Range<usize>>, |
|
| 377 |
+ new_text: &str, |
|
| 378 |
+ new_selected_range_utf16: Option<Range<usize>>, |
|
| 379 |
+ _: &mut Window, |
|
| 380 |
+ cx: &mut Context<Self>, |
|
| 381 |
+ ) {
|
|
| 382 |
+ let range = range_utf16 |
|
| 383 |
+ .as_ref() |
|
| 384 |
+ .map(|range_utf16| self.range_from_utf16(range_utf16)) |
|
| 385 |
+ .or(self.marked_range.clone()) |
|
| 386 |
+ .unwrap_or(self.selected_range.clone()); |
|
| 387 |
+ |
|
| 388 |
+ self.content = |
|
| 389 |
+ (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..]) |
|
| 390 |
+ .into(); |
|
| 391 |
+ self.marked_range = |
|
| 392 |
+ (!new_text.is_empty()).then_some(range.start..range.start + new_text.len()); |
|
| 393 |
+ self.selected_range = new_selected_range_utf16 |
|
| 394 |
+ .as_ref() |
|
| 395 |
+ .map(|range_utf16| self.range_from_utf16(range_utf16)) |
|
| 396 |
+ .map(|new_range| new_range.start + range.start..new_range.end + range.end) |
|
| 397 |
+ .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len()); |
|
| 398 |
+ self.selection_reversed = false; |
|
| 399 |
+ cx.notify(); |
|
| 400 |
+ } |
|
| 401 |
+ |
|
| 402 |
+ fn bounds_for_range( |
|
| 403 |
+ &mut self, |
|
| 404 |
+ range_utf16: Range<usize>, |
|
| 405 |
+ bounds: Bounds<Pixels>, |
|
| 406 |
+ _: &mut Window, |
|
| 407 |
+ _: &mut Context<Self>, |
|
| 408 |
+ ) -> Option<Bounds<Pixels>> {
|
|
| 409 |
+ let last_layout = self.last_layout.as_ref()?; |
|
| 410 |
+ let range = self.range_from_utf16(&range_utf16); |
|
| 411 |
+ Some(Bounds::from_corners( |
|
| 412 |
+ point( |
|
| 413 |
+ bounds.left() + last_layout.x_for_index(range.start) - self.scroll_x, |
|
| 414 |
+ bounds.top(), |
|
| 415 |
+ ), |
|
| 416 |
+ point( |
|
| 417 |
+ bounds.left() + last_layout.x_for_index(range.end) - self.scroll_x, |
|
| 418 |
+ bounds.bottom(), |
|
| 419 |
+ ), |
|
| 420 |
+ )) |
|
| 421 |
+ } |
|
| 422 |
+ |
|
| 423 |
+ fn character_index_for_point( |
|
| 424 |
+ &mut self, |
|
| 425 |
+ point: Point<Pixels>, |
|
| 426 |
+ _: &mut Window, |
|
| 427 |
+ _: &mut Context<Self>, |
|
| 428 |
+ ) -> Option<usize> {
|
|
| 429 |
+ let line_point = self.last_bounds?.localize(&point)?; |
|
| 430 |
+ let last_layout = self.last_layout.as_ref()?; |
|
| 431 |
+ let utf8_index = last_layout.index_for_x(line_point.x + self.scroll_x)?; |
|
| 432 |
+ Some(self.offset_to_utf16(utf8_index)) |
|
| 433 |
+ } |
|
| 434 |
+} |
|
| 435 |
+ |
|
| 436 |
+impl Render for EditLine {
|
|
| 437 |
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
| 438 |
+ div() |
|
| 439 |
+ .key_context(CONTEXT) |
|
| 440 |
+ .track_focus(&self.focus_handle(cx)) |
|
| 441 |
+ .cursor(CursorStyle::IBeam) |
|
| 442 |
+ .on_action(cx.listener(Self::backspace)) |
|
| 443 |
+ .on_action(cx.listener(Self::delete)) |
|
| 444 |
+ .on_action(cx.listener(Self::left)) |
|
| 445 |
+ .on_action(cx.listener(Self::right)) |
|
| 446 |
+ .on_action(cx.listener(Self::select_left)) |
|
| 447 |
+ .on_action(cx.listener(Self::select_right)) |
|
| 448 |
+ .on_action(cx.listener(Self::select_all)) |
|
| 449 |
+ .on_action(cx.listener(Self::home)) |
|
| 450 |
+ .on_action(cx.listener(Self::end)) |
|
| 451 |
+ .on_action(cx.listener(Self::paste)) |
|
| 452 |
+ .on_action(cx.listener(Self::cut)) |
|
| 453 |
+ .on_action(cx.listener(Self::copy)) |
|
| 454 |
+ .on_action(cx.listener(Self::show_character_palette)) |
|
| 455 |
+ .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) |
|
| 456 |
+ .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) |
|
| 457 |
+ .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up)) |
|
| 458 |
+ .on_mouse_move(cx.listener(Self::on_mouse_move)) |
|
| 459 |
+ .min_w_0() |
|
| 460 |
+ .w_full() |
|
| 461 |
+ .child(EditLineElement { input: cx.entity() })
|
|
| 462 |
+ } |
|
| 463 |
+} |
|
| 464 |
+ |
|
| 465 |
+impl Focusable for EditLine {
|
|
| 466 |
+ fn focus_handle(&self, _: &App) -> FocusHandle {
|
|
| 467 |
+ self.focus_handle.clone() |
|
| 468 |
+ } |
|
| 469 |
+} |
|
| 470 |
+ |
|
| 471 |
+struct EditLineElement {
|
|
| 472 |
+ input: Entity<EditLine>, |
|
| 473 |
+} |
|
| 474 |
+ |
|
| 475 |
+struct PrepaintState {
|
|
| 476 |
+ line: Option<ShapedLine>, |
|
| 477 |
+ cursor: Option<PaintQuad>, |
|
| 478 |
+ selection: Option<PaintQuad>, |
|
| 479 |
+ scroll_x: Pixels, |
|
| 480 |
+} |
|
| 481 |
+ |
|
| 482 |
+impl IntoElement for EditLineElement {
|
|
| 483 |
+ type Element = Self; |
|
| 484 |
+ |
|
| 485 |
+ fn into_element(self) -> Self::Element {
|
|
| 486 |
+ self |
|
| 487 |
+ } |
|
| 488 |
+} |
|
| 489 |
+ |
|
| 490 |
+impl Element for EditLineElement {
|
|
| 491 |
+ type RequestLayoutState = (); |
|
| 492 |
+ type PrepaintState = PrepaintState; |
|
| 493 |
+ |
|
| 494 |
+ fn id(&self) -> Option<ElementId> {
|
|
| 495 |
+ None |
|
| 496 |
+ } |
|
| 497 |
+ |
|
| 498 |
+ fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
|
| 499 |
+ None |
|
| 500 |
+ } |
|
| 501 |
+ |
|
| 502 |
+ fn request_layout( |
|
| 503 |
+ &mut self, |
|
| 504 |
+ _: Option<&GlobalElementId>, |
|
| 505 |
+ _: Option<&gpui::InspectorElementId>, |
|
| 506 |
+ window: &mut Window, |
|
| 507 |
+ cx: &mut App, |
|
| 508 |
+ ) -> (LayoutId, Self::RequestLayoutState) {
|
|
| 509 |
+ let mut style = Style::default(); |
|
| 510 |
+ style.size.width = relative(1.).into(); |
|
| 511 |
+ style.size.height = window.line_height().into(); |
|
| 512 |
+ (window.request_layout(style, [], cx), ()) |
|
| 513 |
+ } |
|
| 514 |
+ |
|
| 515 |
+ fn prepaint( |
|
| 516 |
+ &mut self, |
|
| 517 |
+ _: Option<&GlobalElementId>, |
|
| 518 |
+ _: Option<&gpui::InspectorElementId>, |
|
| 519 |
+ bounds: Bounds<Pixels>, |
|
| 520 |
+ _: &mut Self::RequestLayoutState, |
|
| 521 |
+ window: &mut Window, |
|
| 522 |
+ cx: &mut App, |
|
| 523 |
+ ) -> Self::PrepaintState {
|
|
| 524 |
+ let input = self.input.read(cx); |
|
| 525 |
+ let content = if input.masked {
|
|
| 526 |
+ "*".repeat(input.content.len()).into() |
|
| 527 |
+ } else {
|
|
| 528 |
+ input.content.clone() |
|
| 529 |
+ }; |
|
| 530 |
+ let selected_range = input.selected_range.clone(); |
|
| 531 |
+ let cursor = input.cursor_offset(); |
|
| 532 |
+ let previous_scroll_x = input.scroll_x; |
|
| 533 |
+ let style = window.text_style(); |
|
| 534 |
+ |
|
| 535 |
+ let run = TextRun {
|
|
| 536 |
+ len: content.len(), |
|
| 537 |
+ font: style.font(), |
|
| 538 |
+ color: style.color, |
|
| 539 |
+ background_color: None, |
|
| 540 |
+ underline: None, |
|
| 541 |
+ strikethrough: None, |
|
| 542 |
+ }; |
|
| 543 |
+ let runs = if let Some(marked_range) = input.marked_range.as_ref() {
|
|
| 544 |
+ vec![ |
|
| 545 |
+ TextRun {
|
|
| 546 |
+ len: marked_range.start, |
|
| 547 |
+ ..run.clone() |
|
| 548 |
+ }, |
|
| 549 |
+ TextRun {
|
|
| 550 |
+ len: marked_range.end - marked_range.start, |
|
| 551 |
+ underline: Some(UnderlineStyle {
|
|
| 552 |
+ color: Some(run.color), |
|
| 553 |
+ thickness: px(1.), |
|
| 554 |
+ wavy: false, |
|
| 555 |
+ }), |
|
| 556 |
+ ..run.clone() |
|
| 557 |
+ }, |
|
| 558 |
+ TextRun {
|
|
| 559 |
+ len: content.len() - marked_range.end, |
|
| 560 |
+ ..run |
|
| 561 |
+ }, |
|
| 562 |
+ ] |
|
| 563 |
+ .into_iter() |
|
| 564 |
+ .filter(|run| run.len > 0) |
|
| 565 |
+ .collect() |
|
| 566 |
+ } else {
|
|
| 567 |
+ vec![run] |
|
| 568 |
+ }; |
|
| 569 |
+ |
|
| 570 |
+ let font_size = style.font_size.to_pixels(window.rem_size()); |
|
| 571 |
+ let line = window |
|
| 572 |
+ .text_system() |
|
| 573 |
+ .shape_line(content, font_size, &runs, None); |
|
| 574 |
+ let cursor_pos = line.x_for_index(cursor); |
|
| 575 |
+ let scroll_x = adjusted_scroll_x( |
|
| 576 |
+ previous_scroll_x, |
|
| 577 |
+ line.width(), |
|
| 578 |
+ bounds.size.width, |
|
| 579 |
+ cursor_pos, |
|
| 580 |
+ ); |
|
| 581 |
+ let (selection, cursor) = if selected_range.is_empty() {
|
|
| 582 |
+ ( |
|
| 583 |
+ None, |
|
| 584 |
+ Some(fill( |
|
| 585 |
+ Bounds::new( |
|
| 586 |
+ point(bounds.left() + cursor_pos - scroll_x, bounds.top() + px(2.)), |
|
| 587 |
+ size(px(1.), bounds.bottom() - bounds.top() - px(4.)), |
|
| 588 |
+ ), |
|
| 589 |
+ rgb(0x173f45), |
|
| 590 |
+ )), |
|
| 591 |
+ ) |
|
| 592 |
+ } else {
|
|
| 593 |
+ ( |
|
| 594 |
+ Some(fill( |
|
| 595 |
+ Bounds::from_corners( |
|
| 596 |
+ point( |
|
| 597 |
+ bounds.left() + line.x_for_index(selected_range.start) - scroll_x, |
|
| 598 |
+ bounds.top() + px(1.), |
|
| 599 |
+ ), |
|
| 600 |
+ point( |
|
| 601 |
+ bounds.left() + line.x_for_index(selected_range.end) - scroll_x, |
|
| 602 |
+ bounds.bottom() - px(1.), |
|
| 603 |
+ ), |
|
| 604 |
+ ), |
|
| 605 |
+ rgba(0x33298791), |
|
| 606 |
+ )), |
|
| 607 |
+ None, |
|
| 608 |
+ ) |
|
| 609 |
+ }; |
|
| 610 |
+ |
|
| 611 |
+ PrepaintState {
|
|
| 612 |
+ line: Some(line), |
|
| 613 |
+ cursor, |
|
| 614 |
+ selection, |
|
| 615 |
+ scroll_x, |
|
| 616 |
+ } |
|
| 617 |
+ } |
|
| 618 |
+ |
|
| 619 |
+ fn paint( |
|
| 620 |
+ &mut self, |
|
| 621 |
+ _: Option<&GlobalElementId>, |
|
| 622 |
+ _: Option<&gpui::InspectorElementId>, |
|
| 623 |
+ bounds: Bounds<Pixels>, |
|
| 624 |
+ _: &mut Self::RequestLayoutState, |
|
| 625 |
+ prepaint: &mut Self::PrepaintState, |
|
| 626 |
+ window: &mut Window, |
|
| 627 |
+ cx: &mut App, |
|
| 628 |
+ ) {
|
|
| 629 |
+ let focus_handle = self.input.read(cx).focus_handle.clone(); |
|
| 630 |
+ window.handle_input( |
|
| 631 |
+ &focus_handle, |
|
| 632 |
+ ElementInputHandler::new(bounds, self.input.clone()), |
|
| 633 |
+ cx, |
|
| 634 |
+ ); |
|
| 635 |
+ if let Some(selection) = prepaint.selection.take() {
|
|
| 636 |
+ window.paint_quad(selection); |
|
| 637 |
+ } |
|
| 638 |
+ let line = prepaint.line.take().unwrap(); |
|
| 639 |
+ let scroll_x = prepaint.scroll_x; |
|
| 640 |
+ line.paint( |
|
| 641 |
+ point(bounds.origin.x - scroll_x, bounds.origin.y), |
|
| 642 |
+ window.line_height(), |
|
| 643 |
+ gpui::TextAlign::Left, |
|
| 644 |
+ None, |
|
| 645 |
+ window, |
|
| 646 |
+ cx, |
|
| 647 |
+ ) |
|
| 648 |
+ .unwrap(); |
|
| 649 |
+ |
|
| 650 |
+ if focus_handle.is_focused(window) {
|
|
| 651 |
+ if let Some(cursor) = prepaint.cursor.take() {
|
|
| 652 |
+ window.paint_quad(cursor); |
|
| 653 |
+ } |
|
| 654 |
+ } |
|
| 655 |
+ |
|
| 656 |
+ self.input.update(cx, |input, _| {
|
|
| 657 |
+ input.last_layout = Some(line); |
|
| 658 |
+ input.last_bounds = Some(bounds); |
|
| 659 |
+ input.scroll_x = scroll_x; |
|
| 660 |
+ }); |
|
| 661 |
+ } |
|
| 662 |
+} |
|
| 663 |
+ |
|
| 664 |
+fn adjusted_scroll_x( |
|
| 665 |
+ previous: Pixels, |
|
| 666 |
+ line_width: Pixels, |
|
| 667 |
+ visible_width: Pixels, |
|
| 668 |
+ cursor_x: Pixels, |
|
| 669 |
+) -> Pixels {
|
|
| 670 |
+ let padding = px(4.); |
|
| 671 |
+ if line_width.as_f32() <= visible_width.as_f32() {
|
|
| 672 |
+ return Pixels::ZERO; |
|
| 673 |
+ } |
|
| 674 |
+ |
|
| 675 |
+ let mut scroll_x = previous; |
|
| 676 |
+ let right_edge = scroll_x + visible_width - padding; |
|
| 677 |
+ if cursor_x.as_f32() > right_edge.as_f32() {
|
|
| 678 |
+ scroll_x = cursor_x - visible_width + padding; |
|
| 679 |
+ } |
|
| 680 |
+ |
|
| 681 |
+ let left_edge = scroll_x + padding; |
|
| 682 |
+ if cursor_x.as_f32() < left_edge.as_f32() {
|
|
| 683 |
+ scroll_x = cursor_x - padding; |
|
| 684 |
+ } |
|
| 685 |
+ |
|
| 686 |
+ let max_scroll = line_width - visible_width + padding; |
|
| 687 |
+ if scroll_x.as_f32() < 0. {
|
|
| 688 |
+ Pixels::ZERO |
|
| 689 |
+ } else if scroll_x.as_f32() > max_scroll.as_f32() {
|
|
| 690 |
+ max_scroll |
|
| 691 |
+ } else {
|
|
| 692 |
+ scroll_x |
|
| 693 |
+ } |
|
| 694 |
+} |
|
| 695 |
+ |
|
| 696 |
+fn snap_to_char_boundary(value: &str, mut cursor: usize) -> usize {
|
|
| 697 |
+ cursor = cursor.min(value.len()); |
|
| 698 |
+ while !value.is_char_boundary(cursor) {
|
|
| 699 |
+ cursor -= 1; |
|
| 700 |
+ } |
|
| 701 |
+ cursor |
|
| 702 |
+} |
| ... | ... |
@@ -0,0 +1,323 @@ |
| 1 |
+use crate::models::{
|
|
| 2 |
+ AppEntitlement, AppMetadata, AppOption, EntitlementValue, PatchOption, SupportedDeviceFamily, |
|
| 3 |
+}; |
|
| 4 |
+use crate::paths::app_data_dir; |
|
| 5 |
+use async_zip::tokio::read::fs::ZipFileReader; |
|
| 6 |
+use futures_lite::io::AsyncReadExt; |
|
| 7 |
+use plist::{Dictionary, Value};
|
|
| 8 |
+use std::fs; |
|
| 9 |
+use std::io::Cursor; |
|
| 10 |
+use std::path::{Path, PathBuf};
|
|
| 11 |
+ |
|
| 12 |
+pub(crate) fn read_ipa(path: &Path, patches: Vec<PatchOption>) -> Result<AppOption, String> {
|
|
| 13 |
+ tokio::runtime::Builder::new_current_thread() |
|
| 14 |
+ .enable_all() |
|
| 15 |
+ .build() |
|
| 16 |
+ .map_err(|error| format!("Failed to start IPA reader runtime: {error}"))?
|
|
| 17 |
+ .block_on(read_ipa_async(path, patches)) |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+async fn read_ipa_async(path: &Path, patches: Vec<PatchOption>) -> Result<AppOption, String> {
|
|
| 21 |
+ let reader = ZipFileReader::new(path) |
|
| 22 |
+ .await |
|
| 23 |
+ .map_err(|error| format!("Failed to read IPA archive at {}: {error}", path.display()))?;
|
|
| 24 |
+ |
|
| 25 |
+ let info_index = reader |
|
| 26 |
+ .file() |
|
| 27 |
+ .entries() |
|
| 28 |
+ .iter() |
|
| 29 |
+ .position(|entry| entry_name(entry).is_some_and(is_app_info_plist)) |
|
| 30 |
+ .ok_or_else(|| "IPA does not contain Payload/*.app/Info.plist.".to_string())?; |
|
| 31 |
+ let info_path = entry_name(&reader.file().entries()[info_index]) |
|
| 32 |
+ .ok_or_else(|| "IPA Info.plist entry name is invalid.".to_string())?; |
|
| 33 |
+ let app_prefix = info_path |
|
| 34 |
+ .strip_suffix("Info.plist")
|
|
| 35 |
+ .ok_or_else(|| "IPA Info.plist entry path is invalid.".to_string())? |
|
| 36 |
+ .to_string(); |
|
| 37 |
+ |
|
| 38 |
+ let info_bytes = read_entry(&reader, info_index, "Info.plist").await?; |
|
| 39 |
+ let info = plist::from_bytes::<Dictionary>(&info_bytes) |
|
| 40 |
+ .map_err(|error| format!("Failed to parse IPA Info.plist: {error}"))?;
|
|
| 41 |
+ |
|
| 42 |
+ let bundle_id = required_plist_string(&info, "CFBundleIdentifier")?; |
|
| 43 |
+ let executable = required_plist_string(&info, "CFBundleExecutable")?; |
|
| 44 |
+ let version = required_plist_string(&info, "CFBundleShortVersionString")?; |
|
| 45 |
+ let build = required_plist_string(&info, "CFBundleVersion")?; |
|
| 46 |
+ let minimum_os = required_plist_string(&info, "MinimumOSVersion")?; |
|
| 47 |
+ let name = plist_string(&info, "CFBundleDisplayName") |
|
| 48 |
+ .or_else(|| plist_string(&info, "CFBundleName")) |
|
| 49 |
+ .unwrap_or(executable); |
|
| 50 |
+ let supported_devices = supported_devices(&info); |
|
| 51 |
+ |
|
| 52 |
+ let icon_path = extract_icon(&reader, &app_prefix, &info, bundle_id).await; |
|
| 53 |
+ let entitlements = extract_entitlements(&reader, &app_prefix) |
|
| 54 |
+ .await |
|
| 55 |
+ .unwrap_or_else(|| fallback_entitlements(bundle_id)); |
|
| 56 |
+ |
|
| 57 |
+ Ok(AppOption {
|
|
| 58 |
+ metadata: AppMetadata::sample( |
|
| 59 |
+ name, |
|
| 60 |
+ bundle_id, |
|
| 61 |
+ version, |
|
| 62 |
+ build, |
|
| 63 |
+ executable, |
|
| 64 |
+ minimum_os, |
|
| 65 |
+ supported_devices, |
|
| 66 |
+ ), |
|
| 67 |
+ path: path.to_string_lossy().to_string().into(), |
|
| 68 |
+ icon_path: icon_path.map(|path| path.to_string_lossy().to_string().into()), |
|
| 69 |
+ icon_override_path: None, |
|
| 70 |
+ entitlements, |
|
| 71 |
+ entitlement_overrides: None, |
|
| 72 |
+ patches, |
|
| 73 |
+ }) |
|
| 74 |
+} |
|
| 75 |
+ |
|
| 76 |
+fn is_app_info_plist(path: &str) -> bool {
|
|
| 77 |
+ path.starts_with("Payload/")
|
|
| 78 |
+ && path.ends_with(".app/Info.plist")
|
|
| 79 |
+ && path["Payload/".len()..].contains(".app/")
|
|
| 80 |
+} |
|
| 81 |
+ |
|
| 82 |
+fn entry_name(entry: &async_zip::ZipEntry) -> Option<&str> {
|
|
| 83 |
+ entry.filename().as_str().ok() |
|
| 84 |
+} |
|
| 85 |
+ |
|
| 86 |
+fn required_plist_string<'a>(info: &'a Dictionary, key: &str) -> Result<&'a str, String> {
|
|
| 87 |
+ plist_string(info, key).ok_or_else(|| format!("IPA Info.plist is missing string key {key}."))
|
|
| 88 |
+} |
|
| 89 |
+ |
|
| 90 |
+async fn read_entry(reader: &ZipFileReader, index: usize, label: &str) -> Result<Vec<u8>, String> {
|
|
| 91 |
+ let mut entry_reader = reader |
|
| 92 |
+ .reader_without_entry(index) |
|
| 93 |
+ .await |
|
| 94 |
+ .map_err(|error| format!("Failed to read {label} from IPA: {error}"))?;
|
|
| 95 |
+ let mut data = Vec::new(); |
|
| 96 |
+ entry_reader |
|
| 97 |
+ .read_to_end(&mut data) |
|
| 98 |
+ .await |
|
| 99 |
+ .map_err(|error| format!("Failed to extract {label} from IPA: {error}"))?;
|
|
| 100 |
+ Ok(data) |
|
| 101 |
+} |
|
| 102 |
+ |
|
| 103 |
+fn plist_string<'a>(plist: &'a Dictionary, key: &str) -> Option<&'a str> {
|
|
| 104 |
+ plist.get(key).and_then(Value::as_string) |
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+fn supported_devices(info: &Dictionary) -> Vec<SupportedDeviceFamily> {
|
|
| 108 |
+ let Some(Value::Array(families)) = info.get("UIDeviceFamily") else {
|
|
| 109 |
+ return Vec::new(); |
|
| 110 |
+ }; |
|
| 111 |
+ |
|
| 112 |
+ let mut devices = families |
|
| 113 |
+ .iter() |
|
| 114 |
+ .filter_map(|value| value.as_unsigned_integer()) |
|
| 115 |
+ .filter_map(SupportedDeviceFamily::from_device_family_id) |
|
| 116 |
+ .collect::<Vec<_>>(); |
|
| 117 |
+ devices.sort(); |
|
| 118 |
+ devices.dedup(); |
|
| 119 |
+ devices |
|
| 120 |
+} |
|
| 121 |
+ |
|
| 122 |
+async fn extract_icon( |
|
| 123 |
+ reader: &ZipFileReader, |
|
| 124 |
+ app_prefix: &str, |
|
| 125 |
+ info: &Dictionary, |
|
| 126 |
+ bundle_id: &str, |
|
| 127 |
+) -> Option<PathBuf> {
|
|
| 128 |
+ let mut candidates = icon_candidates(info); |
|
| 129 |
+ candidates.reverse(); |
|
| 130 |
+ |
|
| 131 |
+ for candidate in candidates {
|
|
| 132 |
+ if let Some((index, name)) = find_icon_entry(reader, app_prefix, &candidate) {
|
|
| 133 |
+ if let Ok(bytes) = read_entry(reader, index, name).await {
|
|
| 134 |
+ if let Some(path) = cache_icon(bundle_id, &bytes) {
|
|
| 135 |
+ return Some(path); |
|
| 136 |
+ } |
|
| 137 |
+ } |
|
| 138 |
+ } |
|
| 139 |
+ } |
|
| 140 |
+ |
|
| 141 |
+ let fallback = reader |
|
| 142 |
+ .file() |
|
| 143 |
+ .entries() |
|
| 144 |
+ .iter() |
|
| 145 |
+ .enumerate() |
|
| 146 |
+ .filter_map(|(index, entry)| {
|
|
| 147 |
+ let name = entry_name(entry)?; |
|
| 148 |
+ let lower = name.to_ascii_lowercase(); |
|
| 149 |
+ (name.starts_with(app_prefix) |
|
| 150 |
+ && lower.ends_with(".png")
|
|
| 151 |
+ && (lower.contains("appicon") || lower.contains("icon")))
|
|
| 152 |
+ .then_some((index, name, entry.compressed_size())) |
|
| 153 |
+ }) |
|
| 154 |
+ .max_by_key(|(_, _, size)| *size) |
|
| 155 |
+ .map(|(index, name, _)| (index, name)); |
|
| 156 |
+ |
|
| 157 |
+ let Some((index, name)) = fallback else {
|
|
| 158 |
+ return None; |
|
| 159 |
+ }; |
|
| 160 |
+ let bytes = read_entry(reader, index, name).await.ok()?; |
|
| 161 |
+ cache_icon(bundle_id, &bytes) |
|
| 162 |
+} |
|
| 163 |
+ |
|
| 164 |
+fn icon_candidates(info: &Dictionary) -> Vec<String> {
|
|
| 165 |
+ let mut candidates = Vec::new(); |
|
| 166 |
+ append_icon_candidates(info.get("CFBundleIconFiles"), &mut candidates);
|
|
| 167 |
+ append_icon_candidates_from_icons(info.get("CFBundleIcons"), &mut candidates);
|
|
| 168 |
+ append_icon_candidates_from_icons(info.get("CFBundleIcons~ipad"), &mut candidates);
|
|
| 169 |
+ candidates |
|
| 170 |
+} |
|
| 171 |
+ |
|
| 172 |
+fn append_icon_candidates(value: Option<&Value>, candidates: &mut Vec<String>) {
|
|
| 173 |
+ match value {
|
|
| 174 |
+ Some(Value::Array(values)) => {
|
|
| 175 |
+ candidates.extend( |
|
| 176 |
+ values |
|
| 177 |
+ .iter() |
|
| 178 |
+ .filter_map(Value::as_string) |
|
| 179 |
+ .map(str::to_string), |
|
| 180 |
+ ); |
|
| 181 |
+ } |
|
| 182 |
+ Some(Value::String(value)) => candidates.push(value.clone()), |
|
| 183 |
+ _ => {}
|
|
| 184 |
+ } |
|
| 185 |
+} |
|
| 186 |
+ |
|
| 187 |
+fn append_icon_candidates_from_icons(value: Option<&Value>, candidates: &mut Vec<String>) {
|
|
| 188 |
+ let Some(Value::Dictionary(icons)) = value else {
|
|
| 189 |
+ return; |
|
| 190 |
+ }; |
|
| 191 |
+ let Some(Value::Dictionary(primary_icon)) = icons.get("CFBundlePrimaryIcon") else {
|
|
| 192 |
+ return; |
|
| 193 |
+ }; |
|
| 194 |
+ append_icon_candidates(primary_icon.get("CFBundleIconFiles"), candidates);
|
|
| 195 |
+} |
|
| 196 |
+ |
|
| 197 |
+fn find_icon_entry<'a>( |
|
| 198 |
+ reader: &'a ZipFileReader, |
|
| 199 |
+ app_prefix: &str, |
|
| 200 |
+ candidate: &str, |
|
| 201 |
+) -> Option<(usize, &'a str)> {
|
|
| 202 |
+ let candidate = candidate.trim_start_matches('/');
|
|
| 203 |
+ let forms = if candidate.ends_with(".png") {
|
|
| 204 |
+ vec![candidate.to_string()] |
|
| 205 |
+ } else {
|
|
| 206 |
+ vec![ |
|
| 207 |
+ format!("{candidate}@3x.png"),
|
|
| 208 |
+ format!("{candidate}@2x.png"),
|
|
| 209 |
+ format!("{candidate}.png"),
|
|
| 210 |
+ candidate.to_string(), |
|
| 211 |
+ ] |
|
| 212 |
+ }; |
|
| 213 |
+ |
|
| 214 |
+ reader |
|
| 215 |
+ .file() |
|
| 216 |
+ .entries() |
|
| 217 |
+ .iter() |
|
| 218 |
+ .enumerate() |
|
| 219 |
+ .find_map(|(index, entry)| {
|
|
| 220 |
+ let name = entry_name(entry)?; |
|
| 221 |
+ let relative = name.strip_prefix(app_prefix)?; |
|
| 222 |
+ forms |
|
| 223 |
+ .iter() |
|
| 224 |
+ .any(|form| relative == form || relative.ends_with(&format!("/{form}")))
|
|
| 225 |
+ .then_some((index, name)) |
|
| 226 |
+ }) |
|
| 227 |
+} |
|
| 228 |
+ |
|
| 229 |
+fn cache_icon(bundle_id: &str, bytes: &[u8]) -> Option<PathBuf> {
|
|
| 230 |
+ let mut path = app_data_dir()?.join("icons");
|
|
| 231 |
+ fs::create_dir_all(&path).ok()?; |
|
| 232 |
+ path.push(format!("{}.png", cache_safe_name(bundle_id)));
|
|
| 233 |
+ fs::write(&path, bytes).ok()?; |
|
| 234 |
+ Some(path) |
|
| 235 |
+} |
|
| 236 |
+ |
|
| 237 |
+fn cache_safe_name(value: &str) -> String {
|
|
| 238 |
+ value |
|
| 239 |
+ .chars() |
|
| 240 |
+ .map(|character| {
|
|
| 241 |
+ if character.is_ascii_alphanumeric() || matches!(character, '.' | '-' | '_') {
|
|
| 242 |
+ character |
|
| 243 |
+ } else {
|
|
| 244 |
+ '_' |
|
| 245 |
+ } |
|
| 246 |
+ }) |
|
| 247 |
+ .collect() |
|
| 248 |
+} |
|
| 249 |
+ |
|
| 250 |
+async fn extract_entitlements( |
|
| 251 |
+ reader: &ZipFileReader, |
|
| 252 |
+ app_prefix: &str, |
|
| 253 |
+) -> Option<Vec<AppEntitlement>> {
|
|
| 254 |
+ let entry_index = reader.file().entries().iter().position(|entry| {
|
|
| 255 |
+ entry_name(entry) == Some(&format!("{app_prefix}embedded.mobileprovision"))
|
|
| 256 |
+ })?; |
|
| 257 |
+ let bytes = read_entry(reader, entry_index, "embedded.mobileprovision") |
|
| 258 |
+ .await |
|
| 259 |
+ .ok()?; |
|
| 260 |
+ let plist = embedded_plist(&bytes)?; |
|
| 261 |
+ let entitlements = plist.get("Entitlements")?.as_dictionary()?;
|
|
| 262 |
+ Some(entitlements_from_dictionary(entitlements)) |
|
| 263 |
+} |
|
| 264 |
+ |
|
| 265 |
+fn embedded_plist(bytes: &[u8]) -> Option<Dictionary> {
|
|
| 266 |
+ let start = find_bytes(bytes, b"<?xml")?; |
|
| 267 |
+ let end = find_bytes(&bytes[start..], b"</plist>")? + start + b"</plist>".len(); |
|
| 268 |
+ plist::from_reader(Cursor::new(&bytes[start..end])).ok() |
|
| 269 |
+} |
|
| 270 |
+ |
|
| 271 |
+fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
|
| 272 |
+ haystack |
|
| 273 |
+ .windows(needle.len()) |
|
| 274 |
+ .position(|window| window == needle) |
|
| 275 |
+} |
|
| 276 |
+ |
|
| 277 |
+fn entitlements_from_dictionary(entitlements: &Dictionary) -> Vec<AppEntitlement> {
|
|
| 278 |
+ entitlements |
|
| 279 |
+ .iter() |
|
| 280 |
+ .map(|(key, value)| AppEntitlement {
|
|
| 281 |
+ key: key.clone().into(), |
|
| 282 |
+ value: entitlement_value(value), |
|
| 283 |
+ }) |
|
| 284 |
+ .collect() |
|
| 285 |
+} |
|
| 286 |
+ |
|
| 287 |
+fn entitlement_value(value: &Value) -> EntitlementValue {
|
|
| 288 |
+ match value {
|
|
| 289 |
+ Value::String(value) => EntitlementValue::String(value.clone().into()), |
|
| 290 |
+ Value::Boolean(value) => EntitlementValue::Boolean(*value), |
|
| 291 |
+ Value::Integer(value) => value |
|
| 292 |
+ .as_signed() |
|
| 293 |
+ .map(EntitlementValue::Integer) |
|
| 294 |
+ .unwrap_or_else(|| EntitlementValue::String(value.to_string().into())), |
|
| 295 |
+ Value::Real(value) => EntitlementValue::Number(*value), |
|
| 296 |
+ Value::Array(values) => {
|
|
| 297 |
+ EntitlementValue::Array(values.iter().map(entitlement_value).collect()) |
|
| 298 |
+ } |
|
| 299 |
+ Value::Dictionary(values) => EntitlementValue::Dictionary( |
|
| 300 |
+ values |
|
| 301 |
+ .iter() |
|
| 302 |
+ .map(|(key, value)| (key.clone().into(), entitlement_value(value))) |
|
| 303 |
+ .collect(), |
|
| 304 |
+ ), |
|
| 305 |
+ Value::Data(value) => EntitlementValue::Data(value.clone()), |
|
| 306 |
+ Value::Date(value) => EntitlementValue::Date(value.to_xml_format().into()), |
|
| 307 |
+ Value::Uid(value) => EntitlementValue::Uid(value.get()), |
|
| 308 |
+ _ => EntitlementValue::Unknown("Value".into()),
|
|
| 309 |
+ } |
|
| 310 |
+} |
|
| 311 |
+ |
|
| 312 |
+fn fallback_entitlements(bundle_id: &str) -> Vec<AppEntitlement> {
|
|
| 313 |
+ vec![ |
|
| 314 |
+ AppEntitlement {
|
|
| 315 |
+ key: "application-identifier".into(), |
|
| 316 |
+ value: EntitlementValue::String(format!("TEAMID.{bundle_id}").into()),
|
|
| 317 |
+ }, |
|
| 318 |
+ AppEntitlement {
|
|
| 319 |
+ key: "get-task-allow".into(), |
|
| 320 |
+ value: EntitlementValue::Boolean(true), |
|
| 321 |
+ }, |
|
| 322 |
+ ] |
|
| 323 |
+} |
| ... | ... |
@@ -1,23 +1,37 @@ |
| 1 |
+mod adi_backend; |
|
| 2 |
+mod adi_services; |
|
| 3 |
+mod app_selection; |
|
| 1 | 4 |
mod assets; |
| 2 | 5 |
mod constants; |
| 3 | 6 |
mod data; |
| 7 |
+mod developer_accounts; |
|
| 8 |
+mod device_discovery; |
|
| 9 |
+mod device_selection; |
|
| 10 |
+mod edit_line; |
|
| 11 |
+mod ipa; |
|
| 4 | 12 |
mod main_view; |
| 5 | 13 |
mod models; |
| 14 |
+mod paths; |
|
| 15 |
+mod preferences; |
|
| 6 | 16 |
mod settings; |
| 17 |
+mod system_identity; |
|
| 7 | 18 |
mod widgets; |
| 8 | 19 |
|
| 9 | 20 |
use assets::Assets; |
| 10 | 21 |
use constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
|
| 11 | 22 |
use gpui::{px, size, App, AppContext, Bounds, QuitMode, WindowBounds, WindowOptions};
|
| 23 |
+use log::LevelFilter; |
|
| 12 | 24 |
use main_view::SideloaderView; |
| 13 | 25 |
|
| 14 | 26 |
fn main() {
|
| 15 |
- env_logger::init(); |
|
| 27 |
+ init_logging(); |
|
| 16 | 28 |
|
| 17 | 29 |
gpui_platform::application() |
| 18 | 30 |
.with_assets(Assets) |
| 19 | 31 |
.with_quit_mode(QuitMode::LastWindowClosed) |
| 20 | 32 |
.run(|cx: &mut App| {
|
| 33 |
+ edit_line::init(cx); |
|
| 34 |
+ |
|
| 21 | 35 |
let window_size = size(px(WINDOW_WIDTH), px(WINDOW_HEIGHT)); |
| 22 | 36 |
let bounds = Bounds::centered(None, window_size, cx); |
| 23 | 37 |
|
| ... | ... |
@@ -38,3 +52,14 @@ fn main() {
|
| 38 | 52 |
cx.activate(true); |
| 39 | 53 |
}); |
| 40 | 54 |
} |
| 55 |
+ |
|
| 56 |
+fn init_logging() {
|
|
| 57 |
+ let mut builder = env_logger::Builder::from_default_env(); |
|
| 58 |
+ builder |
|
| 59 |
+ .filter_module("reqwest", LevelFilter::Info)
|
|
| 60 |
+ .filter_module("hyper", LevelFilter::Info)
|
|
| 61 |
+ .filter_module("h2", LevelFilter::Info)
|
|
| 62 |
+ .filter_module("rustls", LevelFilter::Info)
|
|
| 63 |
+ .filter_module("elf_loader", LevelFilter::Info);
|
|
| 64 |
+ builder.init(); |
|
| 65 |
+} |
| ... | ... |
@@ -1,10 +1,17 @@ |
| 1 |
+use crate::adi_services; |
|
| 2 |
+use crate::app_selection::{is_ipa_path, load_ipa, AppSelection};
|
|
| 1 | 3 |
use crate::constants::*; |
| 2 |
-use crate::data::{
|
|
| 3 |
- sample_accounts, sample_adi_backends, sample_apps, sample_devices, sample_machine_identity, |
|
| 4 |
-}; |
|
| 4 |
+use crate::data::{load_accounts, load_machine_identity};
|
|
| 5 |
+use crate::device_discovery::{discover_devices, watch_device_changes};
|
|
| 6 |
+use crate::device_selection::DeviceSelection; |
|
| 5 | 7 |
use crate::models::{
|
| 6 |
- AccountOption, AdiBackendOption, AppOption, DeviceOption, MachineIdentity, PickerId, |
|
| 7 |
- SideloadOperation, SideloadPhase, TeamOption, |
|
| 8 |
+ AccountOption, AdiBackendKind, AdiBackendOption, AppOption, DeviceOption, MachineIdentity, |
|
| 9 |
+ PickerId, SideloadOperation, SideloadPhase, TeamOption, |
|
| 10 |
+}; |
|
| 11 |
+use crate::preferences::{
|
|
| 12 |
+ load_preferences, save_preferences as save_preferences_to_disk, AdiPreferences, |
|
| 13 |
+ AppOverridePreferences, AppPreferences, DeveloperPreferences, MachineIdentityPreferences, |
|
| 14 |
+ SideloaderPreferences, StoredAdiBackendKind, |
|
| 8 | 15 |
}; |
| 9 | 16 |
use crate::settings::{show_or_open_settings_window, SettingsWindow, SettingsWindowRequest};
|
| 10 | 17 |
use crate::widgets::{
|
| ... | ... |
@@ -12,24 +19,29 @@ use crate::widgets::{
|
| 12 | 19 |
developer_account_title, device_identity, dropdown_list, lucide_icon, menu_action_row, |
| 13 | 20 |
progress_circle, properties_list, |
| 14 | 21 |
}; |
| 22 |
+use futures::{channel::mpsc, StreamExt};
|
|
| 15 | 23 |
use gpui::{
|
| 16 |
- div, prelude::*, px, rgb, App, ClickEvent, Context, FocusHandle, FontWeight, |
|
| 17 |
- InteractiveElement, IntoElement, ParentElement, Render, StatefulInteractiveElement, Styled, |
|
| 18 |
- Window, WindowHandle, |
|
| 24 |
+ div, prelude::*, px, rgb, App, ClickEvent, Context, ExternalPaths, FocusHandle, FontWeight, |
|
| 25 |
+ InteractiveElement, IntoElement, ParentElement, PathPromptOptions, Render, |
|
| 26 |
+ StatefulInteractiveElement, Styled, Window, WindowHandle, |
|
| 19 | 27 |
}; |
| 28 |
+use rand::RngExt; |
|
| 29 |
+use std::path::PathBuf; |
|
| 20 | 30 |
|
| 21 | 31 |
pub(crate) struct SideloaderView {
|
| 22 | 32 |
pub(crate) focus_handle: FocusHandle, |
| 23 | 33 |
pub(crate) accounts: Vec<AccountOption>, |
| 24 |
- pub(crate) apps: Vec<AppOption>, |
|
| 25 |
- pub(crate) devices: Vec<DeviceOption>, |
|
| 34 |
+ pub(crate) app_selection: AppSelection, |
|
| 35 |
+ pub(crate) device_selection: DeviceSelection, |
|
| 26 | 36 |
pub(crate) adi_backends: Vec<AdiBackendOption>, |
| 27 | 37 |
pub(crate) selected_account: usize, |
| 28 | 38 |
pub(crate) selected_team: usize, |
| 29 |
- pub(crate) selected_app: usize, |
|
| 30 |
- pub(crate) selected_device: usize, |
|
| 39 |
+ pub(crate) auto_app_id: bool, |
|
| 40 |
+ pub(crate) selected_app_id: usize, |
|
| 31 | 41 |
pub(crate) selected_adi_backend: usize, |
| 32 | 42 |
pub(crate) machine_identity: MachineIdentity, |
| 43 |
+ pub(crate) android_device_identity: MachineIdentity, |
|
| 44 |
+ pub(crate) android_adi_identifier: String, |
|
| 33 | 45 |
pub(crate) open_picker: Option<PickerId>, |
| 34 | 46 |
pub(crate) enabled_patches: Vec<bool>, |
| 35 | 47 |
pub(crate) sideload_operation: SideloadOperation, |
| ... | ... |
@@ -45,28 +57,74 @@ impl SideloaderView {
|
| 45 | 57 |
cx.on_release(|view, cx| view.close_child_windows(cx)) |
| 46 | 58 |
.detach(); |
| 47 | 59 |
|
| 48 |
- let accounts = sample_accounts(); |
|
| 49 |
- let apps = sample_apps(); |
|
| 50 |
- let devices = sample_devices(); |
|
| 51 |
- let adi_backends = sample_adi_backends(); |
|
| 52 |
- let machine_identity = sample_machine_identity(); |
|
| 53 |
- let enabled_patches = apps |
|
| 54 |
- .first() |
|
| 60 |
+ let preferences = load_preferences().unwrap_or_else(|error| {
|
|
| 61 |
+ eprintln!("{error}");
|
|
| 62 |
+ SideloaderPreferences::default() |
|
| 63 |
+ }); |
|
| 64 |
+ let accounts = load_accounts(); |
|
| 65 |
+ let app_selection = AppSelection::from_preferences(&preferences.app); |
|
| 66 |
+ let machine_identity = load_machine_identity(); |
|
| 67 |
+ let mut android_device_uuid = preferences |
|
| 68 |
+ .adi |
|
| 69 |
+ .android_device |
|
| 70 |
+ .machine_id |
|
| 71 |
+ .clone() |
|
| 72 |
+ .or_else(|| preferences.adi.android_device_uuid.clone()) |
|
| 73 |
+ .unwrap_or_default(); |
|
| 74 |
+ let generated_android_device_uuid = ensure_android_device_uuid(&mut android_device_uuid); |
|
| 75 |
+ let mut android_device_identity = |
|
| 76 |
+ android_device_identity_from_host(&machine_identity, android_device_uuid); |
|
| 77 |
+ apply_machine_identity_preferences( |
|
| 78 |
+ &mut android_device_identity, |
|
| 79 |
+ &preferences.adi.android_device, |
|
| 80 |
+ ); |
|
| 81 |
+ let mut android_adi_identifier = preferences |
|
| 82 |
+ .adi |
|
| 83 |
+ .android_adi_identifier |
|
| 84 |
+ .clone() |
|
| 85 |
+ .or_else(|| {
|
|
| 86 |
+ preferences |
|
| 87 |
+ .adi |
|
| 88 |
+ .android_machine |
|
| 89 |
+ .machine_id |
|
| 90 |
+ .clone() |
|
| 91 |
+ .filter(|identifier| identifier.len() == 16) |
|
| 92 |
+ }) |
|
| 93 |
+ .unwrap_or_default(); |
|
| 94 |
+ let generated_android_adi_identifier = |
|
| 95 |
+ ensure_android_adi_identifier(&mut android_adi_identifier); |
|
| 96 |
+ let device_selection = DeviceSelection::new(preferences.device.as_ref()); |
|
| 97 |
+ let adi_backends = adi_services::available_backends(&android_adi_identifier); |
|
| 98 |
+ let selected_account = selected_account_index(&accounts, &preferences.developer); |
|
| 99 |
+ let selected_team = |
|
| 100 |
+ selected_team_index(&accounts, selected_account, &preferences.developer); |
|
| 101 |
+ let selected_app_id = selected_app_id_index( |
|
| 102 |
+ &accounts, |
|
| 103 |
+ selected_account, |
|
| 104 |
+ selected_team, |
|
| 105 |
+ &preferences.developer, |
|
| 106 |
+ ); |
|
| 107 |
+ let selected_adi_backend = |
|
| 108 |
+ selected_adi_backend_index(&adi_backends, preferences.adi.backend); |
|
| 109 |
+ let enabled_patches = app_selection |
|
| 110 |
+ .selected() |
|
| 55 | 111 |
.map(|app| vec![false; app.patches.len()]) |
| 56 | 112 |
.unwrap_or_default(); |
| 57 | 113 |
|
| 58 |
- Self {
|
|
| 114 |
+ let mut view = Self {
|
|
| 59 | 115 |
focus_handle, |
| 60 | 116 |
accounts, |
| 61 |
- apps, |
|
| 62 |
- devices, |
|
| 117 |
+ app_selection, |
|
| 118 |
+ device_selection, |
|
| 63 | 119 |
adi_backends, |
| 64 |
- selected_account: 0, |
|
| 65 |
- selected_team: 0, |
|
| 66 |
- selected_app: 0, |
|
| 67 |
- selected_device: 0, |
|
| 68 |
- selected_adi_backend: 0, |
|
| 120 |
+ selected_account, |
|
| 121 |
+ selected_team, |
|
| 122 |
+ auto_app_id: preferences.developer.auto_app_id, |
|
| 123 |
+ selected_app_id, |
|
| 124 |
+ selected_adi_backend, |
|
| 69 | 125 |
machine_identity, |
| 126 |
+ android_device_identity, |
|
| 127 |
+ android_adi_identifier, |
|
| 70 | 128 |
open_picker: None, |
| 71 | 129 |
enabled_patches, |
| 72 | 130 |
sideload_operation: SideloadOperation::Idle, |
| ... | ... |
@@ -74,7 +132,13 @@ impl SideloaderView {
|
| 74 | 132 |
team_settings_window: None, |
| 75 | 133 |
app_settings_window: None, |
| 76 | 134 |
adi_settings_window: None, |
| 135 |
+ }; |
|
| 136 |
+ if generated_android_adi_identifier || generated_android_device_uuid {
|
|
| 137 |
+ view.save_preferences(); |
|
| 77 | 138 |
} |
| 139 |
+ view.start_device_refresh(cx); |
|
| 140 |
+ view.start_device_events_watch(cx); |
|
| 141 |
+ view |
|
| 78 | 142 |
} |
| 79 | 143 |
|
| 80 | 144 |
fn close_child_windows(&mut self, cx: &mut App) {
|
| ... | ... |
@@ -84,45 +148,168 @@ impl SideloaderView {
|
| 84 | 148 |
} |
| 85 | 149 |
|
| 86 | 150 |
fn is_busy(&self) -> bool {
|
| 87 |
- self.sideload_operation.is_busy() |
|
| 151 |
+ self.sideload_operation.is_busy() || self.app_selection.is_loading() |
|
| 88 | 152 |
} |
| 89 | 153 |
|
| 90 |
- fn selected_account(&self) -> &AccountOption {
|
|
| 91 |
- &self.accounts[self.selected_account] |
|
| 154 |
+ pub(crate) fn save_preferences(&self) {
|
|
| 155 |
+ if let Err(error) = save_preferences_to_disk(&self.preferences()) {
|
|
| 156 |
+ eprintln!("{error}");
|
|
| 92 | 157 |
} |
| 93 |
- |
|
| 94 |
- fn selected_team(&self) -> &TeamOption {
|
|
| 95 |
- &self.selected_account().teams[self.selected_team] |
|
| 96 | 158 |
} |
| 97 | 159 |
|
| 98 |
- fn selected_app(&self) -> &AppOption {
|
|
| 99 |
- &self.apps[self.selected_app] |
|
| 160 |
+ pub(crate) fn select_team_from_settings( |
|
| 161 |
+ &mut self, |
|
| 162 |
+ index: usize, |
|
| 163 |
+ cx: &mut Context<Self>, |
|
| 164 |
+ ) -> Option<gpui::SharedString> {
|
|
| 165 |
+ let team_count = self.accounts.get(self.selected_account)?.teams.len(); |
|
| 166 |
+ if index >= team_count {
|
|
| 167 |
+ return None; |
|
| 100 | 168 |
} |
| 101 | 169 |
|
| 102 |
- fn selected_device(&self) -> &DeviceOption {
|
|
| 103 |
- &self.devices[self.selected_device] |
|
| 170 |
+ self.selected_team = index; |
|
| 171 |
+ self.selected_app_id = 0; |
|
| 172 |
+ self.save_preferences(); |
|
| 173 |
+ cx.notify(); |
|
| 174 |
+ self.selected_team().map(|team| team.identifier.clone()) |
|
| 104 | 175 |
} |
| 105 | 176 |
|
| 106 |
- fn select_account( |
|
| 177 |
+ pub(crate) fn set_auto_app_id_from_settings( |
|
| 107 | 178 |
&mut self, |
| 108 |
- index: usize, |
|
| 109 |
- _: &ClickEvent, |
|
| 110 |
- window: &mut Window, |
|
| 179 |
+ auto_app_id: bool, |
|
| 111 | 180 |
cx: &mut Context<Self>, |
| 112 | 181 |
) {
|
| 113 |
- window.focus(&self.focus_handle, cx); |
|
| 114 |
- if self.is_busy() {
|
|
| 115 |
- return; |
|
| 182 |
+ self.auto_app_id = auto_app_id; |
|
| 183 |
+ self.save_preferences(); |
|
| 184 |
+ cx.notify(); |
|
| 116 | 185 |
} |
| 117 |
- if index < self.accounts.len() {
|
|
| 118 |
- self.selected_account = index; |
|
| 186 |
+ |
|
| 187 |
+ pub(crate) fn select_app_id_from_settings(&mut self, index: usize, cx: &mut Context<Self>) {
|
|
| 188 |
+ self.selected_app_id = index; |
|
| 189 |
+ self.save_preferences(); |
|
| 190 |
+ cx.notify(); |
|
| 191 |
+ } |
|
| 192 |
+ |
|
| 193 |
+ pub(crate) fn add_developer_account_from_settings( |
|
| 194 |
+ &mut self, |
|
| 195 |
+ account: AccountOption, |
|
| 196 |
+ cx: &mut Context<Self>, |
|
| 197 |
+ ) -> Option<SettingsWindowRequest> {
|
|
| 198 |
+ self.accounts.push(account); |
|
| 199 |
+ self.selected_account = self.accounts.len().saturating_sub(1); |
|
| 119 | 200 |
self.selected_team = 0; |
| 201 |
+ self.selected_app_id = 0; |
|
| 120 | 202 |
self.open_picker = None; |
| 121 | 203 |
cx.notify(); |
| 204 |
+ |
|
| 205 |
+ let account = self.accounts.get(self.selected_account)?; |
|
| 206 |
+ Some(SettingsWindowRequest::Team {
|
|
| 207 |
+ parent: cx.weak_entity(), |
|
| 208 |
+ teams: account.teams.clone(), |
|
| 209 |
+ selected_team: self.selected_team, |
|
| 210 |
+ auto_app_id: self.auto_app_id, |
|
| 211 |
+ selected_app_id: self.selected_app_id, |
|
| 212 |
+ }) |
|
| 213 |
+ } |
|
| 214 |
+ |
|
| 215 |
+ pub(crate) fn select_adi_backend_from_settings( |
|
| 216 |
+ &mut self, |
|
| 217 |
+ index: usize, |
|
| 218 |
+ cx: &mut Context<Self>, |
|
| 219 |
+ ) {
|
|
| 220 |
+ if index < self.adi_backends.len() {
|
|
| 221 |
+ self.selected_adi_backend = index; |
|
| 222 |
+ self.save_preferences(); |
|
| 223 |
+ cx.notify(); |
|
| 224 |
+ } |
|
| 225 |
+ } |
|
| 226 |
+ |
|
| 227 |
+ pub(crate) fn replace_app_from_settings( |
|
| 228 |
+ &mut self, |
|
| 229 |
+ app_index: usize, |
|
| 230 |
+ app: AppOption, |
|
| 231 |
+ cx: &mut Context<Self>, |
|
| 232 |
+ ) {
|
|
| 233 |
+ if self.app_selection.replace(app_index, app) {
|
|
| 234 |
+ self.save_preferences(); |
|
| 235 |
+ cx.notify(); |
|
| 236 |
+ } |
|
| 237 |
+ } |
|
| 238 |
+ |
|
| 239 |
+ pub(crate) fn replace_adi_backends_from_settings( |
|
| 240 |
+ &mut self, |
|
| 241 |
+ backends: Vec<AdiBackendOption>, |
|
| 242 |
+ selected_backend: usize, |
|
| 243 |
+ persist_selection: bool, |
|
| 244 |
+ cx: &mut Context<Self>, |
|
| 245 |
+ ) {
|
|
| 246 |
+ self.adi_backends = backends; |
|
| 247 |
+ self.selected_adi_backend = selected_backend; |
|
| 248 |
+ if persist_selection {
|
|
| 249 |
+ self.save_preferences(); |
|
| 122 | 250 |
} |
| 251 |
+ cx.notify(); |
|
| 123 | 252 |
} |
| 124 | 253 |
|
| 125 |
- fn select_app( |
|
| 254 |
+ pub(crate) fn replace_android_device_identity_from_settings( |
|
| 255 |
+ &mut self, |
|
| 256 |
+ identity: MachineIdentity, |
|
| 257 |
+ cx: &mut Context<Self>, |
|
| 258 |
+ ) {
|
|
| 259 |
+ self.android_device_identity = identity; |
|
| 260 |
+ self.save_preferences(); |
|
| 261 |
+ cx.notify(); |
|
| 262 |
+ } |
|
| 263 |
+ |
|
| 264 |
+ fn preferences(&self) -> SideloaderPreferences {
|
|
| 265 |
+ let account = self.accounts.get(self.selected_account); |
|
| 266 |
+ let team = account.and_then(|account| account.teams.get(self.selected_team)); |
|
| 267 |
+ let app_id = team.and_then(|team| team.app_ids.get(self.selected_app_id)); |
|
| 268 |
+ let app = self.selected_app(); |
|
| 269 |
+ let backend = self.adi_backends.get(self.selected_adi_backend); |
|
| 270 |
+ |
|
| 271 |
+ SideloaderPreferences {
|
|
| 272 |
+ developer: DeveloperPreferences {
|
|
| 273 |
+ account_id: account.map(|account| account.id.to_string()), |
|
| 274 |
+ team_id: team.map(|team| team.identifier.to_string()), |
|
| 275 |
+ auto_app_id: self.auto_app_id, |
|
| 276 |
+ app_id: app_id.map(|app_id| app_id.identifier.to_string()), |
|
| 277 |
+ }, |
|
| 278 |
+ app: AppPreferences {
|
|
| 279 |
+ bundle_id: app.map(|app| app.bundle_id().to_string()), |
|
| 280 |
+ path: self.app_selection.selected_path_for_preferences(), |
|
| 281 |
+ overrides: app.map(AppOverridePreferences::from).unwrap_or_default(), |
|
| 282 |
+ }, |
|
| 283 |
+ device: self.device_selection.selected_preferences(), |
|
| 284 |
+ adi: AdiPreferences {
|
|
| 285 |
+ backend: backend.map(|backend| StoredAdiBackendKind::from(backend.kind)), |
|
| 286 |
+ machine: MachineIdentityPreferences::from(&self.machine_identity), |
|
| 287 |
+ android_adi_identifier: Some(self.android_adi_identifier.clone()), |
|
| 288 |
+ android_device: MachineIdentityPreferences::from(&self.android_device_identity), |
|
| 289 |
+ android_device_uuid: Some(self.android_device_identity.machine_id.to_string()), |
|
| 290 |
+ android_machine: MachineIdentityPreferences::default(), |
|
| 291 |
+ }, |
|
| 292 |
+ } |
|
| 293 |
+ } |
|
| 294 |
+ |
|
| 295 |
+ fn selected_account(&self) -> Option<&AccountOption> {
|
|
| 296 |
+ self.accounts.get(self.selected_account) |
|
| 297 |
+ } |
|
| 298 |
+ |
|
| 299 |
+ fn selected_team(&self) -> Option<&TeamOption> {
|
|
| 300 |
+ self.selected_account() |
|
| 301 |
+ .and_then(|account| account.teams.get(self.selected_team)) |
|
| 302 |
+ } |
|
| 303 |
+ |
|
| 304 |
+ fn selected_app(&self) -> Option<&AppOption> {
|
|
| 305 |
+ self.app_selection.selected() |
|
| 306 |
+ } |
|
| 307 |
+ |
|
| 308 |
+ fn selected_device(&self) -> Option<&DeviceOption> {
|
|
| 309 |
+ self.device_selection.selected() |
|
| 310 |
+ } |
|
| 311 |
+ |
|
| 312 |
+ fn select_account( |
|
| 126 | 313 |
&mut self, |
| 127 | 314 |
index: usize, |
| 128 | 315 |
_: &ClickEvent, |
| ... | ... |
@@ -133,10 +320,12 @@ impl SideloaderView {
|
| 133 | 320 |
if self.is_busy() {
|
| 134 | 321 |
return; |
| 135 | 322 |
} |
| 136 |
- if index < self.apps.len() {
|
|
| 137 |
- self.selected_app = index; |
|
| 138 |
- self.enabled_patches = vec![false; self.selected_app().patches.len()]; |
|
| 323 |
+ if index < self.accounts.len() {
|
|
| 324 |
+ self.selected_account = index; |
|
| 325 |
+ self.selected_team = 0; |
|
| 326 |
+ self.selected_app_id = 0; |
|
| 139 | 327 |
self.open_picker = None; |
| 328 |
+ self.save_preferences(); |
|
| 140 | 329 |
cx.notify(); |
| 141 | 330 |
} |
| 142 | 331 |
} |
| ... | ... |
@@ -152,9 +341,9 @@ impl SideloaderView {
|
| 152 | 341 |
if self.is_busy() {
|
| 153 | 342 |
return; |
| 154 | 343 |
} |
| 155 |
- if index < self.devices.len() {
|
|
| 156 |
- self.selected_device = index; |
|
| 344 |
+ if self.device_selection.select(index) {
|
|
| 157 | 345 |
self.open_picker = None; |
| 346 |
+ self.save_preferences(); |
|
| 158 | 347 |
cx.notify(); |
| 159 | 348 |
} |
| 160 | 349 |
} |
| ... | ... |
@@ -165,6 +354,16 @@ impl SideloaderView {
|
| 165 | 354 |
return; |
| 166 | 355 |
} |
| 167 | 356 |
self.open_picker = None; |
| 357 |
+ let request = SettingsWindowRequest::DeveloperLogin {
|
|
| 358 |
+ parent: cx.weak_entity(), |
|
| 359 |
+ }; |
|
| 360 |
+ self.team_settings_window = Some(show_or_open_settings_window( |
|
| 361 |
+ self.team_settings_window, |
|
| 362 |
+ request, |
|
| 363 |
+ SETTINGS_WINDOW_WIDTH, |
|
| 364 |
+ TEAM_SETTINGS_WINDOW_HEIGHT, |
|
| 365 |
+ cx, |
|
| 366 |
+ )); |
|
| 168 | 367 |
cx.notify(); |
| 169 | 368 |
} |
| 170 | 369 |
|
| ... | ... |
@@ -174,14 +373,103 @@ impl SideloaderView {
|
| 174 | 373 |
return; |
| 175 | 374 |
} |
| 176 | 375 |
self.open_picker = None; |
| 376 |
+ let receiver = cx.prompt_for_paths(PathPromptOptions {
|
|
| 377 |
+ files: true, |
|
| 378 |
+ directories: false, |
|
| 379 |
+ multiple: false, |
|
| 380 |
+ prompt: Some("Select IPA".into()),
|
|
| 381 |
+ }); |
|
| 382 |
+ |
|
| 383 |
+ cx.spawn(async move |view, cx| {
|
|
| 384 |
+ let paths = match receiver.await {
|
|
| 385 |
+ Ok(Ok(Some(paths))) => paths, |
|
| 386 |
+ Ok(Ok(None)) | Err(_) => return, |
|
| 387 |
+ Ok(Err(error)) => {
|
|
| 388 |
+ eprintln!("Failed to select IPA: {error}");
|
|
| 389 |
+ return; |
|
| 390 |
+ } |
|
| 391 |
+ }; |
|
| 392 |
+ |
|
| 393 |
+ let Some(path) = paths.into_iter().next() else {
|
|
| 394 |
+ return; |
|
| 395 |
+ }; |
|
| 396 |
+ |
|
| 397 |
+ let _ = view.update(cx, |view, cx| {
|
|
| 398 |
+ view.select_ipa_path(path, cx); |
|
| 399 |
+ }); |
|
| 400 |
+ }) |
|
| 401 |
+ .detach(); |
|
| 177 | 402 |
cx.notify(); |
| 178 | 403 |
} |
| 179 | 404 |
|
| 180 |
- fn sideload(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 405 |
+ fn drop_ipa(&mut self, paths: &ExternalPaths, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 181 | 406 |
window.focus(&self.focus_handle, cx); |
| 407 |
+ cx.stop_propagation(); |
|
| 182 | 408 |
if self.is_busy() {
|
| 183 | 409 |
return; |
| 184 | 410 |
} |
| 411 |
+ if let Some(path) = first_ipa_path(paths) {
|
|
| 412 |
+ self.select_ipa_path(path, cx); |
|
| 413 |
+ } |
|
| 414 |
+ } |
|
| 415 |
+ |
|
| 416 |
+ fn select_ipa_path(&mut self, path: PathBuf, cx: &mut Context<Self>) {
|
|
| 417 |
+ if !is_ipa_path(&path) {
|
|
| 418 |
+ let path = path.to_string_lossy().to_string(); |
|
| 419 |
+ self.app_selection.fail( |
|
| 420 |
+ path.clone(), |
|
| 421 |
+ format!("Selected file is not an IPA archive: {path}"),
|
|
| 422 |
+ ); |
|
| 423 |
+ self.enabled_patches.clear(); |
|
| 424 |
+ self.save_preferences(); |
|
| 425 |
+ cx.notify(); |
|
| 426 |
+ return; |
|
| 427 |
+ } |
|
| 428 |
+ |
|
| 429 |
+ let path_string = path.to_string_lossy().to_string(); |
|
| 430 |
+ if self.app_selection.select_cached_path(&path_string) {
|
|
| 431 |
+ self.enabled_patches = self |
|
| 432 |
+ .selected_app() |
|
| 433 |
+ .map(|app| vec![false; app.patches.len()]) |
|
| 434 |
+ .unwrap_or_default(); |
|
| 435 |
+ self.open_picker = None; |
|
| 436 |
+ self.save_preferences(); |
|
| 437 |
+ cx.notify(); |
|
| 438 |
+ return; |
|
| 439 |
+ } |
|
| 440 |
+ |
|
| 441 |
+ let patches = self |
|
| 442 |
+ .selected_app() |
|
| 443 |
+ .map(|app| app.patches.clone()) |
|
| 444 |
+ .unwrap_or_default(); |
|
| 445 |
+ self.app_selection.begin_loading(path_string); |
|
| 446 |
+ self.open_picker = None; |
|
| 447 |
+ cx.notify(); |
|
| 448 |
+ |
|
| 449 |
+ cx.spawn(async move |view, cx| {
|
|
| 450 |
+ let path_for_error = path.clone(); |
|
| 451 |
+ let result = cx |
|
| 452 |
+ .background_spawn(async move { load_ipa(path, patches.clone()) })
|
|
| 453 |
+ .await; |
|
| 454 |
+ |
|
| 455 |
+ let _ = view.update(cx, |view, cx| {
|
|
| 456 |
+ view.app_selection.finish_loading(&path_for_error, result); |
|
| 457 |
+ view.enabled_patches = view |
|
| 458 |
+ .selected_app() |
|
| 459 |
+ .map(|app| vec![false; app.patches.len()]) |
|
| 460 |
+ .unwrap_or_default(); |
|
| 461 |
+ view.save_preferences(); |
|
| 462 |
+ cx.notify(); |
|
| 463 |
+ }); |
|
| 464 |
+ }) |
|
| 465 |
+ .detach(); |
|
| 466 |
+ } |
|
| 467 |
+ |
|
| 468 |
+ fn sideload(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 469 |
+ window.focus(&self.focus_handle, cx); |
|
| 470 |
+ if self.is_busy() || self.selected_app().is_none() || self.selected_device().is_none() {
|
|
| 471 |
+ return; |
|
| 472 |
+ } |
|
| 185 | 473 |
self.open_picker = None; |
| 186 | 474 |
self.sideload_operation = SideloadOperation::Running {
|
| 187 | 475 |
phase: SideloadPhase::Signing, |
| ... | ... |
@@ -190,17 +478,27 @@ impl SideloaderView {
|
| 190 | 478 |
cx.notify(); |
| 191 | 479 |
} |
| 192 | 480 |
|
| 193 |
- fn toggle_account_picker( |
|
| 481 |
+ fn refresh_devices_from_button( |
|
| 194 | 482 |
&mut self, |
| 195 | 483 |
_: &ClickEvent, |
| 196 | 484 |
window: &mut Window, |
| 197 | 485 |
cx: &mut Context<Self>, |
| 198 | 486 |
) {
|
| 199 |
- self.toggle_picker(PickerId::Account, window, cx); |
|
| 487 |
+ window.focus(&self.focus_handle, cx); |
|
| 488 |
+ if self.is_busy() {
|
|
| 489 |
+ return; |
|
| 490 |
+ } |
|
| 491 |
+ self.start_device_events_watch(cx); |
|
| 492 |
+ self.start_device_refresh(cx); |
|
| 200 | 493 |
} |
| 201 | 494 |
|
| 202 |
- fn toggle_app_picker(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 203 |
- self.toggle_picker(PickerId::App, window, cx); |
|
| 495 |
+ fn toggle_account_picker( |
|
| 496 |
+ &mut self, |
|
| 497 |
+ _: &ClickEvent, |
|
| 498 |
+ window: &mut Window, |
|
| 499 |
+ cx: &mut Context<Self>, |
|
| 500 |
+ ) {
|
|
| 501 |
+ self.toggle_picker(PickerId::Account, window, cx); |
|
| 204 | 502 |
} |
| 205 | 503 |
|
| 206 | 504 |
fn toggle_device_picker( |
| ... | ... |
@@ -230,14 +528,21 @@ impl SideloaderView {
|
| 230 | 528 |
if self.is_busy() {
|
| 231 | 529 |
return; |
| 232 | 530 |
} |
| 531 |
+ let Some(account) = self.selected_account() else {
|
|
| 532 |
+ return; |
|
| 533 |
+ }; |
|
| 534 |
+ if account.teams.is_empty() || self.selected_team().is_none() {
|
|
| 535 |
+ return; |
|
| 536 |
+ } |
|
| 537 |
+ let teams = account.teams.clone(); |
|
| 233 | 538 |
self.open_picker = None; |
| 234 | 539 |
let parent = cx.weak_entity(); |
| 235 | 540 |
let request = SettingsWindowRequest::Team {
|
| 236 | 541 |
parent, |
| 237 |
- teams: self.selected_account().teams.clone(), |
|
| 542 |
+ teams, |
|
| 238 | 543 |
selected_team: self.selected_team, |
| 239 |
- auto_app_id: true, |
|
| 240 |
- selected_app_id: 0, |
|
| 544 |
+ auto_app_id: self.auto_app_id, |
|
| 545 |
+ selected_app_id: self.selected_app_id, |
|
| 241 | 546 |
}; |
| 242 | 547 |
self.team_settings_window = Some(show_or_open_settings_window( |
| 243 | 548 |
self.team_settings_window, |
| ... | ... |
@@ -249,17 +554,93 @@ impl SideloaderView {
|
| 249 | 554 |
cx.notify(); |
| 250 | 555 |
} |
| 251 | 556 |
|
| 557 |
+ fn start_device_refresh(&mut self, cx: &mut Context<Self>) {
|
|
| 558 |
+ let Some(generation) = self.device_selection.begin_refresh() else {
|
|
| 559 |
+ return; |
|
| 560 |
+ }; |
|
| 561 |
+ cx.notify(); |
|
| 562 |
+ |
|
| 563 |
+ cx.spawn(async move |view, cx| {
|
|
| 564 |
+ let result = cx.background_spawn(async move { discover_devices() }).await;
|
|
| 565 |
+ let _ = view.update(cx, |view, cx| {
|
|
| 566 |
+ view.finish_device_refresh(generation, result); |
|
| 567 |
+ cx.notify(); |
|
| 568 |
+ }); |
|
| 569 |
+ }) |
|
| 570 |
+ .detach(); |
|
| 571 |
+ } |
|
| 572 |
+ |
|
| 573 |
+ fn start_device_events_watch(&mut self, cx: &mut Context<Self>) {
|
|
| 574 |
+ if !self.device_selection.start_events_watch() {
|
|
| 575 |
+ return; |
|
| 576 |
+ } |
|
| 577 |
+ |
|
| 578 |
+ let (sender, mut receiver) = mpsc::unbounded(); |
|
| 579 |
+ cx.background_spawn(async move { watch_device_changes(sender) })
|
|
| 580 |
+ .detach(); |
|
| 581 |
+ |
|
| 582 |
+ cx.spawn(async move |view, cx| {
|
|
| 583 |
+ while let Some(result) = receiver.next().await {
|
|
| 584 |
+ let should_continue = view |
|
| 585 |
+ .update(cx, |view, cx| match result {
|
|
| 586 |
+ Ok(()) => {
|
|
| 587 |
+ view.device_selection.note_device_event(); |
|
| 588 |
+ if !view.is_busy() && !view.device_selection.is_refreshing() {
|
|
| 589 |
+ view.start_device_refresh(cx); |
|
| 590 |
+ } |
|
| 591 |
+ true |
|
| 592 |
+ } |
|
| 593 |
+ Err(error) => {
|
|
| 594 |
+ view.device_selection.fail_events_watch(error); |
|
| 595 |
+ cx.notify(); |
|
| 596 |
+ false |
|
| 597 |
+ } |
|
| 598 |
+ }) |
|
| 599 |
+ .unwrap_or(false); |
|
| 600 |
+ |
|
| 601 |
+ if !should_continue {
|
|
| 602 |
+ return; |
|
| 603 |
+ } |
|
| 604 |
+ } |
|
| 605 |
+ |
|
| 606 |
+ let _ = view.update(cx, |view, cx| {
|
|
| 607 |
+ view.device_selection.finish_events_watch(); |
|
| 608 |
+ cx.notify(); |
|
| 609 |
+ }); |
|
| 610 |
+ }) |
|
| 611 |
+ .detach(); |
|
| 612 |
+ } |
|
| 613 |
+ |
|
| 614 |
+ fn finish_device_refresh( |
|
| 615 |
+ &mut self, |
|
| 616 |
+ generation: u64, |
|
| 617 |
+ result: Result<Vec<DeviceOption>, String>, |
|
| 618 |
+ ) {
|
|
| 619 |
+ self.device_selection.finish_refresh(generation, result); |
|
| 620 |
+ if self.device_selection.is_empty() && self.open_picker == Some(PickerId::Device) {
|
|
| 621 |
+ self.open_picker = None; |
|
| 622 |
+ } |
|
| 623 |
+ } |
|
| 624 |
+ |
|
| 252 | 625 |
fn open_app_settings(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
| 253 | 626 |
window.focus(&self.focus_handle, cx); |
| 254 | 627 |
if self.is_busy() {
|
| 255 | 628 |
return; |
| 256 | 629 |
} |
| 630 |
+ let Some(app) = self.selected_app().cloned() else {
|
|
| 631 |
+ return; |
|
| 632 |
+ }; |
|
| 257 | 633 |
self.open_picker = None; |
| 258 | 634 |
let parent = cx.weak_entity(); |
| 259 | 635 |
let request = SettingsWindowRequest::AppSettings {
|
| 260 |
- _parent: parent, |
|
| 261 |
- app: self.selected_app().clone(), |
|
| 636 |
+ parent, |
|
| 637 |
+ app_index: self.app_selection.selected_index(), |
|
| 638 |
+ app, |
|
| 262 | 639 |
enabled_patches: self.enabled_patches.clone(), |
| 640 |
+ team_id: self |
|
| 641 |
+ .selected_team() |
|
| 642 |
+ .map(|team| team.identifier.clone()) |
|
| 643 |
+ .unwrap_or_default(), |
|
| 263 | 644 |
}; |
| 264 | 645 |
self.app_settings_window = Some(show_or_open_settings_window( |
| 265 | 646 |
self.app_settings_window, |
| ... | ... |
@@ -274,12 +655,15 @@ impl SideloaderView {
|
| 274 | 655 |
fn open_adi_settings(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
| 275 | 656 |
window.focus(&self.focus_handle, cx); |
| 276 | 657 |
self.open_picker = None; |
| 658 |
+ self.refresh_adi_backends(); |
|
| 277 | 659 |
let parent = cx.weak_entity(); |
| 278 | 660 |
let request = SettingsWindowRequest::AdiSettings {
|
| 279 | 661 |
parent, |
| 280 | 662 |
backends: self.adi_backends.clone(), |
| 281 | 663 |
selected_backend: self.selected_adi_backend, |
| 282 | 664 |
machine_identity: self.machine_identity.clone(), |
| 665 |
+ android_device_identity: self.android_device_identity.clone(), |
|
| 666 |
+ android_adi_identifier: self.android_adi_identifier.clone(), |
|
| 283 | 667 |
}; |
| 284 | 668 |
self.adi_settings_window = Some(show_or_open_settings_window( |
| 285 | 669 |
self.adi_settings_window, |
| ... | ... |
@@ -291,9 +675,26 @@ impl SideloaderView {
|
| 291 | 675 |
cx.notify(); |
| 292 | 676 |
} |
| 293 | 677 |
|
| 678 |
+ pub(crate) fn refresh_adi_backends(&mut self) {
|
|
| 679 |
+ let selected_kind = self |
|
| 680 |
+ .adi_backends |
|
| 681 |
+ .get(self.selected_adi_backend) |
|
| 682 |
+ .map(|backend| backend.kind); |
|
| 683 |
+ self.adi_backends = adi_services::available_backends(&self.android_adi_identifier); |
|
| 684 |
+ self.selected_adi_backend = selected_kind |
|
| 685 |
+ .and_then(|kind| {
|
|
| 686 |
+ self.adi_backends |
|
| 687 |
+ .iter() |
|
| 688 |
+ .position(|backend| backend.kind == kind) |
|
| 689 |
+ }) |
|
| 690 |
+ .unwrap_or_else(|| adi_services::default_backend(&self.adi_backends)); |
|
| 691 |
+ } |
|
| 692 |
+ |
|
| 294 | 693 |
fn enabled_patch_names(&self) -> Vec<String> {
|
| 295 |
- self.selected_app() |
|
| 296 |
- .patches |
|
| 694 |
+ let Some(app) = self.selected_app() else {
|
|
| 695 |
+ return Vec::new(); |
|
| 696 |
+ }; |
|
| 697 |
+ app.patches |
|
| 297 | 698 |
.iter() |
| 298 | 699 |
.enumerate() |
| 299 | 700 |
.filter_map(|(index, patch)| {
|
| ... | ... |
@@ -310,8 +711,22 @@ impl SideloaderView {
|
| 310 | 711 |
number: &'static str, |
| 311 | 712 |
title: &'static str, |
| 312 | 713 |
detail: &'static str, |
| 313 |
- ) -> impl IntoElement {
|
|
| 314 |
- div().flex().items_start().justify_between().gap_3().child( |
|
| 714 |
+ ) -> gpui::Div {
|
|
| 715 |
+ Self::section_header_with_action(number, title, detail, div()) |
|
| 716 |
+ } |
|
| 717 |
+ |
|
| 718 |
+ fn section_header_with_action( |
|
| 719 |
+ number: &'static str, |
|
| 720 |
+ title: &'static str, |
|
| 721 |
+ detail: &'static str, |
|
| 722 |
+ action: impl IntoElement, |
|
| 723 |
+ ) -> gpui::Div {
|
|
| 724 |
+ div() |
|
| 725 |
+ .flex() |
|
| 726 |
+ .items_start() |
|
| 727 |
+ .justify_between() |
|
| 728 |
+ .gap_3() |
|
| 729 |
+ .child( |
|
| 315 | 730 |
div() |
| 316 | 731 |
.flex() |
| 317 | 732 |
.items_center() |
| ... | ... |
@@ -345,6 +760,7 @@ impl SideloaderView {
|
| 345 | 760 |
.child(div().text_xs().text_color(rgb(0x6a7a81)).child(detail)), |
| 346 | 761 |
), |
| 347 | 762 |
) |
| 763 |
+ .child(action) |
|
| 348 | 764 |
} |
| 349 | 765 |
|
| 350 | 766 |
fn settings_button(label: &'static str, detail: &'static str) -> gpui::Div {
|
| ... | ... |
@@ -427,46 +843,12 @@ impl SideloaderView {
|
| 427 | 843 |
)) |
| 428 | 844 |
} |
| 429 | 845 |
|
| 430 |
- fn app_row(&self, index: usize, cx: &mut Context<Self>) -> impl IntoElement {
|
|
| 431 |
- let app = &self.apps[index]; |
|
| 432 |
- let selected = index == self.selected_app; |
|
| 433 |
- let border = if selected {
|
|
| 434 |
- rgb(0x0f6f7a) |
|
| 435 |
- } else {
|
|
| 436 |
- rgb(0xd8e0df) |
|
| 437 |
- }; |
|
| 438 |
- |
|
| 439 |
- div() |
|
| 440 |
- .h(px(COMBO_ITEM_HEIGHT)) |
|
| 441 |
- .px_3() |
|
| 442 |
- .rounded_md() |
|
| 443 |
- .border_1() |
|
| 444 |
- .border_color(border) |
|
| 445 |
- .bg(if selected {
|
|
| 446 |
- rgb(0xf0fbfa) |
|
| 447 |
- } else {
|
|
| 448 |
- rgb(0xffffff) |
|
| 449 |
- }) |
|
| 450 |
- .cursor_pointer() |
|
| 451 |
- .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 452 |
- .id(("app-row", index))
|
|
| 453 |
- .on_click( |
|
| 454 |
- cx.listener(move |this, event, window, cx| {
|
|
| 455 |
- this.select_app(index, event, window, cx) |
|
| 456 |
- }), |
|
| 457 |
- ) |
|
| 458 |
- .flex() |
|
| 459 |
- .items_center() |
|
| 460 |
- .child(combo_item_content( |
|
| 461 |
- app.path.clone(), |
|
| 462 |
- app.name.clone(), |
|
| 463 |
- app_identity(app), |
|
| 464 |
- )) |
|
| 465 |
- } |
|
| 466 |
- |
|
| 467 | 846 |
fn device_row(&self, index: usize, cx: &mut Context<Self>) -> impl IntoElement {
|
| 468 |
- let device = &self.devices[index]; |
|
| 469 |
- let selected = index == self.selected_device; |
|
| 847 |
+ let device = self |
|
| 848 |
+ .device_selection |
|
| 849 |
+ .device(index) |
|
| 850 |
+ .expect("device row index must be valid");
|
|
| 851 |
+ let selected = index == self.device_selection.selected_index(); |
|
| 470 | 852 |
let border = if selected {
|
| 471 | 853 |
rgb(0x0f6f7a) |
| 472 | 854 |
} else {
|
| ... | ... |
@@ -522,12 +904,21 @@ impl SideloaderView {
|
| 522 | 904 |
} |
| 523 | 905 |
|
| 524 | 906 |
fn account_picker(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
| 525 |
- let account = self.selected_account(); |
|
| 907 |
+ let body = if let Some(account) = self.selected_account() {
|
|
| 526 | 908 |
let team = self.selected_team(); |
| 527 | 909 |
let is_open = self.open_picker == Some(PickerId::Account); |
| 910 |
+ let detail = team |
|
| 911 |
+ .map(|team| developer_account_title(&team.role)) |
|
| 912 |
+ .unwrap_or_else(|| account.status.clone()); |
|
| 913 |
+ let mut properties = vec![ |
|
| 914 |
+ ("Apple Account", account.apple_id.to_string()),
|
|
| 915 |
+ ("Status", account.status.to_string()),
|
|
| 916 |
+ ("Source", account.detail.to_string()),
|
|
| 917 |
+ ]; |
|
| 918 |
+ if let Some(team) = team {
|
|
| 919 |
+ properties.push(("Team", team.name.to_string()));
|
|
| 920 |
+ } |
|
| 528 | 921 |
|
| 529 |
- Self::picker_shell( |
|
| 530 |
- Self::section_header("1", "Account", "Apple ID and developer membership"),
|
|
| 531 | 922 |
div() |
| 532 | 923 |
.flex() |
| 533 | 924 |
.flex_col() |
| ... | ... |
@@ -537,7 +928,7 @@ impl SideloaderView {
|
| 537 | 928 |
combo_button( |
| 538 | 929 |
account.apple_id.clone(), |
| 539 | 930 |
account.label.clone(), |
| 540 |
- developer_account_title(&team.role), |
|
| 931 |
+ detail, |
|
| 541 | 932 |
is_open, |
| 542 | 933 |
) |
| 543 | 934 |
.id("account-combobox")
|
| ... | ... |
@@ -548,35 +939,42 @@ impl SideloaderView {
|
| 548 | 939 |
) |
| 549 | 940 |
.child( |
| 550 | 941 |
menu_action_row( |
| 551 |
- "Manage Accounts...", |
|
| 552 |
- "Add, remove, or refresh Apple ID sessions.", |
|
| 942 |
+ "Add an Apple Account", |
|
| 943 |
+ "Log in to another developer Apple Account.", |
|
| 553 | 944 |
) |
| 554 | 945 |
.id("manage-accounts")
|
| 555 | 946 |
.on_click(cx.listener(Self::manage_accounts)), |
| 556 | 947 |
) |
| 557 | 948 |
.id("account-options"),
|
| 558 | 949 |
)) |
| 559 |
- .child(properties_list(vec![ |
|
| 560 |
- ("Apple ID", account.apple_id.to_string()),
|
|
| 561 |
- ("Status", account.status.to_string()),
|
|
| 562 |
- ("Source", account.detail.to_string()),
|
|
| 563 |
- ("Team", team.name.to_string()),
|
|
| 564 |
- ])) |
|
| 565 |
- .child( |
|
| 950 |
+ .child(properties_list(properties)) |
|
| 951 |
+ .when(team.is_some(), |this| {
|
|
| 952 |
+ this.child( |
|
| 566 | 953 |
Self::settings_button( |
| 567 | 954 |
"Advanced Settings", |
| 568 | 955 |
"Select the developer team for signing.", |
| 569 | 956 |
) |
| 570 | 957 |
.id("team-disclosure")
|
| 571 | 958 |
.on_click(cx.listener(Self::open_team_settings)), |
| 572 |
- ), |
|
| 959 |
+ ) |
|
| 960 |
+ }) |
|
| 961 |
+ } else {
|
|
| 962 |
+ div() |
|
| 963 |
+ .flex() |
|
| 964 |
+ .flex_col() |
|
| 965 |
+ .gap_4() |
|
| 966 |
+ .child(add_apple_account_button(cx)) |
|
| 967 |
+ }; |
|
| 968 |
+ |
|
| 969 |
+ Self::picker_shell( |
|
| 970 |
+ Self::section_header("1", "Account", "Apple Account and developer membership"),
|
|
| 971 |
+ body, |
|
| 573 | 972 |
self.is_busy(), |
| 574 | 973 |
) |
| 575 | 974 |
} |
| 576 | 975 |
|
| 577 | 976 |
fn app_picker(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
| 578 | 977 |
let app = self.selected_app(); |
| 579 |
- let is_open = self.open_picker == Some(PickerId::App); |
|
| 580 | 978 |
let patch_summary = self.enabled_patch_names(); |
| 581 | 979 |
let patch_summary = if patch_summary.is_empty() {
|
| 582 | 980 |
"No patches enabled".to_string() |
| ... | ... |
@@ -584,55 +982,157 @@ impl SideloaderView {
|
| 584 | 982 |
patch_summary.join(", ")
|
| 585 | 983 |
}; |
| 586 | 984 |
|
| 587 |
- Self::picker_shell( |
|
| 588 |
- Self::section_header("2", "App", "IPA selection and patch profile"),
|
|
| 589 |
- div() |
|
| 985 |
+ let body = div() |
|
| 590 | 986 |
.flex() |
| 591 | 987 |
.flex_col() |
| 592 | 988 |
.gap_4() |
| 593 |
- .child(combo_with_popover( |
|
| 594 |
- "app-popover-scroll", |
|
| 595 |
- combo_button( |
|
| 596 |
- app.path.clone(), |
|
| 597 |
- app.name.clone(), |
|
| 598 |
- app_identity(app), |
|
| 599 |
- is_open, |
|
| 600 |
- ) |
|
| 601 |
- .id("app-combobox")
|
|
| 602 |
- .on_click(cx.listener(Self::toggle_app_picker)), |
|
| 603 |
- is_open, |
|
| 604 |
- dropdown_list((0..self.apps.len()).map(|index| self.app_row(index, cx))) |
|
| 605 |
- .child( |
|
| 606 |
- menu_action_row("Choose IPA...", "Browse for another IPA archive.")
|
|
| 607 |
- .id("choose-ipa")
|
|
| 608 |
- .on_click(cx.listener(Self::choose_ipa)), |
|
| 989 |
+ .child(self.ipa_button(app, cx)) |
|
| 990 |
+ .when_some(self.app_selection.load_error(), |this, error| {
|
|
| 991 |
+ this.child( |
|
| 992 |
+ div() |
|
| 993 |
+ .rounded_md() |
|
| 994 |
+ .border_1() |
|
| 995 |
+ .border_color(rgb(0xe5b8b2)) |
|
| 996 |
+ .bg(rgb(0xfff5f3)) |
|
| 997 |
+ .p_3() |
|
| 998 |
+ .text_sm() |
|
| 999 |
+ .text_color(rgb(0x9a302b)) |
|
| 1000 |
+ .child(error.to_string()), |
|
| 609 | 1001 |
) |
| 610 |
- .id("app-options"),
|
|
| 611 |
- )) |
|
| 612 |
- .child(properties_list(vec![ |
|
| 613 |
- ("Bundle ID", app.bundle_id.to_string()),
|
|
| 614 |
- ("Version", format!("{} ({})", app.version, app.build)),
|
|
| 1002 |
+ }); |
|
| 1003 |
+ |
|
| 1004 |
+ let body = if let Some(app) = app {
|
|
| 1005 |
+ body.child(properties_list(vec![ |
|
| 1006 |
+ ("Bundle ID", app.bundle_id().to_string()),
|
|
| 1007 |
+ ("Version", format!("{} ({})", app.version(), app.build())),
|
|
| 615 | 1008 |
("IPA", app.path.to_string()),
|
| 616 | 1009 |
("Patches", patch_summary),
|
| 617 | 1010 |
])) |
| 618 | 1011 |
.child( |
| 619 |
- Self::settings_button( |
|
| 620 |
- "Advanced Settings", |
|
| 621 |
- "Edit app metadata and entitlements.", |
|
| 622 |
- ) |
|
| 1012 |
+ Self::settings_button("Advanced Settings", "Edit app metadata and entitlements.")
|
|
| 623 | 1013 |
.id("app-disclosure")
|
| 624 | 1014 |
.on_click(cx.listener(Self::open_app_settings)), |
| 625 |
- ), |
|
| 1015 |
+ ) |
|
| 1016 |
+ } else {
|
|
| 1017 |
+ body |
|
| 1018 |
+ }; |
|
| 1019 |
+ |
|
| 1020 |
+ Self::picker_shell( |
|
| 1021 |
+ Self::section_header("2", "App", "IPA selection and patch profile"),
|
|
| 1022 |
+ body, |
|
| 626 | 1023 |
self.is_busy(), |
| 627 | 1024 |
) |
| 628 | 1025 |
} |
| 629 | 1026 |
|
| 1027 |
+ fn ipa_button(&self, app: Option<&AppOption>, cx: &mut Context<Self>) -> impl IntoElement {
|
|
| 1028 |
+ let disabled = self.is_busy(); |
|
| 1029 |
+ let (label, title, detail) = if let Some(path) = self.app_selection.loading_path() {
|
|
| 1030 |
+ ( |
|
| 1031 |
+ path.to_string().into(), |
|
| 1032 |
+ "Loading IPA...".into(), |
|
| 1033 |
+ "Reading metadata, icon, and entitlements.".into(), |
|
| 1034 |
+ ) |
|
| 1035 |
+ } else if let Some(app) = app {
|
|
| 1036 |
+ (app.path.clone(), app.name().clone(), app_identity(app)) |
|
| 1037 |
+ } else if let Some(path) = self.app_selection.error_path() {
|
|
| 1038 |
+ ( |
|
| 1039 |
+ path.to_string().into(), |
|
| 1040 |
+ "Failed to load IPA".into(), |
|
| 1041 |
+ "Select another IPA archive.".into(), |
|
| 1042 |
+ ) |
|
| 1043 |
+ } else {
|
|
| 1044 |
+ ( |
|
| 1045 |
+ "No IPA selected".into(), |
|
| 1046 |
+ "Choose IPA...".into(), |
|
| 1047 |
+ "Select or drop an IPA archive.".into(), |
|
| 1048 |
+ ) |
|
| 1049 |
+ }; |
|
| 1050 |
+ |
|
| 1051 |
+ div() |
|
| 1052 |
+ .id("ipa-button")
|
|
| 1053 |
+ .h(px(COMBO_ITEM_HEIGHT)) |
|
| 1054 |
+ .px_3() |
|
| 1055 |
+ .rounded_md() |
|
| 1056 |
+ .border_1() |
|
| 1057 |
+ .border_color(rgb(0xcfd8d6)) |
|
| 1058 |
+ .bg(rgb(0xffffff)) |
|
| 1059 |
+ .flex() |
|
| 1060 |
+ .items_center() |
|
| 1061 |
+ .gap_3() |
|
| 1062 |
+ .can_drop(|dragged, _, _| {
|
|
| 1063 |
+ dragged |
|
| 1064 |
+ .downcast_ref::<ExternalPaths>() |
|
| 1065 |
+ .is_some_and(paths_include_ipa) |
|
| 1066 |
+ }) |
|
| 1067 |
+ .drag_over::<ExternalPaths>(|this, paths, _, _| {
|
|
| 1068 |
+ if paths_include_ipa(paths) {
|
|
| 1069 |
+ this.border_color(rgb(0x0f6f7a)).bg(rgb(0xf0fbfa)) |
|
| 1070 |
+ } else {
|
|
| 1071 |
+ this |
|
| 1072 |
+ } |
|
| 1073 |
+ }) |
|
| 1074 |
+ .when(!disabled, |this| {
|
|
| 1075 |
+ this.cursor_pointer() |
|
| 1076 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 1077 |
+ .on_click(cx.listener(Self::choose_ipa)) |
|
| 1078 |
+ .on_drop(cx.listener(Self::drop_ipa)) |
|
| 1079 |
+ }) |
|
| 1080 |
+ .child(combo_item_content(label, title, detail)) |
|
| 1081 |
+ .child( |
|
| 1082 |
+ div() |
|
| 1083 |
+ .flex_none() |
|
| 1084 |
+ .w_7() |
|
| 1085 |
+ .h_7() |
|
| 1086 |
+ .rounded_md() |
|
| 1087 |
+ .bg(rgb(0xebf1f0)) |
|
| 1088 |
+ .text_color(rgb(0x53666d)) |
|
| 1089 |
+ .flex() |
|
| 1090 |
+ .items_center() |
|
| 1091 |
+ .justify_center() |
|
| 1092 |
+ .child(lucide_icon("icons/folder-open.svg")),
|
|
| 1093 |
+ ) |
|
| 1094 |
+ } |
|
| 1095 |
+ |
|
| 1096 |
+ fn device_refresh_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
| 1097 |
+ div() |
|
| 1098 |
+ .id("refresh-devices")
|
|
| 1099 |
+ .w_7() |
|
| 1100 |
+ .h_7() |
|
| 1101 |
+ .rounded_md() |
|
| 1102 |
+ .bg(rgb(0xebf1f0)) |
|
| 1103 |
+ .text_color(rgb(0x53666d)) |
|
| 1104 |
+ .flex() |
|
| 1105 |
+ .items_center() |
|
| 1106 |
+ .justify_center() |
|
| 1107 |
+ .when( |
|
| 1108 |
+ !self.is_busy() && !self.device_selection.is_refreshing(), |
|
| 1109 |
+ |this| {
|
|
| 1110 |
+ this.cursor_pointer() |
|
| 1111 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 1112 |
+ .on_click(cx.listener(Self::refresh_devices_from_button)) |
|
| 1113 |
+ }, |
|
| 1114 |
+ ) |
|
| 1115 |
+ .when(self.is_busy(), |this| this.opacity(0.55)) |
|
| 1116 |
+ .child(if self.device_selection.is_refreshing() {
|
|
| 1117 |
+ div() |
|
| 1118 |
+ .w_4() |
|
| 1119 |
+ .h_4() |
|
| 1120 |
+ .child(progress_circle(0.34, self.spinner_turns)) |
|
| 1121 |
+ } else {
|
|
| 1122 |
+ div().child(lucide_icon("icons/refresh-cw.svg"))
|
|
| 1123 |
+ }) |
|
| 1124 |
+ } |
|
| 1125 |
+ |
|
| 630 | 1126 |
fn device_picker(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
| 631 |
- let device = self.selected_device(); |
|
| 632 | 1127 |
let is_open = self.open_picker == Some(PickerId::Device); |
| 1128 |
+ let header = Self::section_header_with_action( |
|
| 1129 |
+ "3", |
|
| 1130 |
+ "Device", |
|
| 1131 |
+ "Install target", |
|
| 1132 |
+ self.device_refresh_button(cx), |
|
| 1133 |
+ ); |
|
| 633 | 1134 |
|
| 634 |
- Self::picker_shell( |
|
| 635 |
- Self::section_header("3", "Device", "Install target"),
|
|
| 1135 |
+ let body = if let Some(device) = self.selected_device() {
|
|
| 636 | 1136 |
div() |
| 637 | 1137 |
.flex() |
| 638 | 1138 |
.flex_col() |
| ... | ... |
@@ -646,9 +1146,13 @@ impl SideloaderView {
|
| 646 | 1146 |
is_open, |
| 647 | 1147 |
) |
| 648 | 1148 |
.id("device-combobox")
|
| 649 |
- .on_click(cx.listener(Self::toggle_device_picker)), |
|
| 1149 |
+ .when(!self.device_selection.is_refreshing(), |this| {
|
|
| 1150 |
+ this.on_click(cx.listener(Self::toggle_device_picker)) |
|
| 1151 |
+ }), |
|
| 650 | 1152 |
is_open, |
| 651 |
- dropdown_list((0..self.devices.len()).map(|index| self.device_row(index, cx))) |
|
| 1153 |
+ dropdown_list( |
|
| 1154 |
+ (0..self.device_selection.len()).map(|index| self.device_row(index, cx)), |
|
| 1155 |
+ ) |
|
| 652 | 1156 |
.id("device-options"),
|
| 653 | 1157 |
)) |
| 654 | 1158 |
.child(properties_list(vec![ |
| ... | ... |
@@ -656,9 +1160,41 @@ impl SideloaderView {
|
| 656 | 1160 |
("OS", device.os.to_string()),
|
| 657 | 1161 |
("UDID", device.udid.to_string()),
|
| 658 | 1162 |
("Connection", device.connection.to_string()),
|
| 659 |
- ])), |
|
| 660 |
- self.is_busy(), |
|
| 1163 |
+ ])) |
|
| 1164 |
+ } else {
|
|
| 1165 |
+ let status = if self.device_selection.is_refreshing() {
|
|
| 1166 |
+ "Scanning for devices".to_string() |
|
| 1167 |
+ } else if let Some(error) = self.device_selection.refresh_error() {
|
|
| 1168 |
+ error.to_string() |
|
| 1169 |
+ } else {
|
|
| 1170 |
+ "No devices found".to_string() |
|
| 1171 |
+ }; |
|
| 1172 |
+ let detail = if self.device_selection.is_refreshing() {
|
|
| 1173 |
+ "Looking for connected iPhone, iPad, Apple TV, or Apple Watch.".to_string() |
|
| 1174 |
+ } else {
|
|
| 1175 |
+ "Connect a device, then refresh.".to_string() |
|
| 1176 |
+ }; |
|
| 1177 |
+ |
|
| 1178 |
+ div() |
|
| 1179 |
+ .flex() |
|
| 1180 |
+ .flex_col() |
|
| 1181 |
+ .gap_4() |
|
| 1182 |
+ .child( |
|
| 1183 |
+ div() |
|
| 1184 |
+ .h(px(COMBO_ITEM_HEIGHT)) |
|
| 1185 |
+ .px_3() |
|
| 1186 |
+ .rounded_md() |
|
| 1187 |
+ .border_1() |
|
| 1188 |
+ .border_color(rgb(0xcfd8d6)) |
|
| 1189 |
+ .bg(rgb(0xffffff)) |
|
| 1190 |
+ .flex() |
|
| 1191 |
+ .items_center() |
|
| 1192 |
+ .child(combo_item_content("No UDID", "No device connected", detail)),
|
|
| 661 | 1193 |
) |
| 1194 |
+ .child(properties_list(vec![("Status", status)]))
|
|
| 1195 |
+ }; |
|
| 1196 |
+ |
|
| 1197 |
+ Self::picker_shell(header, body, self.is_busy()) |
|
| 662 | 1198 |
} |
| 663 | 1199 |
|
| 664 | 1200 |
fn plus_connector(disabled: bool) -> impl IntoElement {
|
| ... | ... |
@@ -688,12 +1224,14 @@ impl SideloaderView {
|
| 688 | 1224 |
|
| 689 | 1225 |
fn sideload_connector(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
| 690 | 1226 |
let busy = self.sideload_operation.is_busy(); |
| 1227 |
+ let selected_app = self.selected_app(); |
|
| 1228 |
+ let can_sideload = selected_app.is_some() && self.selected_device().is_some(); |
|
| 691 | 1229 |
let progress = self.sideload_operation.progress(); |
| 692 | 1230 |
let track_fill_width = SIDELOAD_BUTTON_WIDTH * progress; |
| 693 | 1231 |
let status_color = self.sideload_operation.status_color(); |
| 694 | 1232 |
let status_text = self |
| 695 | 1233 |
.sideload_operation |
| 696 |
- .status_text(self.selected_app(), self.selected_device()); |
|
| 1234 |
+ .status_text(selected_app, self.selected_device()); |
|
| 697 | 1235 |
|
| 698 | 1236 |
div() |
| 699 | 1237 |
.w(px(SIDELOAD_CONNECTOR_WIDTH)) |
| ... | ... |
@@ -733,11 +1271,12 @@ impl SideloaderView {
|
| 733 | 1271 |
.items_center() |
| 734 | 1272 |
.justify_center() |
| 735 | 1273 |
.gap_1() |
| 736 |
- .when(!busy, |this| {
|
|
| 1274 |
+ .when(!busy && can_sideload, |this| {
|
|
| 737 | 1275 |
this.cursor_pointer() |
| 738 | 1276 |
.hover(|style| style.bg(rgb(0x20545c))) |
| 739 | 1277 |
.on_click(cx.listener(Self::sideload)) |
| 740 | 1278 |
}) |
| 1279 |
+ .when(!can_sideload, |this| this.opacity(0.7)) |
|
| 741 | 1280 |
.when(busy, |this| {
|
| 742 | 1281 |
this.child( |
| 743 | 1282 |
div() |
| ... | ... |
@@ -826,9 +1365,197 @@ fn close_settings_window(handle: Option<WindowHandle<SettingsWindow>>, cx: &mut |
| 826 | 1365 |
} |
| 827 | 1366 |
} |
| 828 | 1367 |
|
| 1368 |
+fn add_apple_account_button(cx: &mut Context<SideloaderView>) -> impl IntoElement {
|
|
| 1369 |
+ div() |
|
| 1370 |
+ .id("add-apple-account")
|
|
| 1371 |
+ .h(px(COMBO_ITEM_HEIGHT)) |
|
| 1372 |
+ .px_3() |
|
| 1373 |
+ .rounded_md() |
|
| 1374 |
+ .border_1() |
|
| 1375 |
+ .border_color(rgb(0xcfd8d6)) |
|
| 1376 |
+ .bg(rgb(0xffffff)) |
|
| 1377 |
+ .cursor_pointer() |
|
| 1378 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 1379 |
+ .flex() |
|
| 1380 |
+ .items_center() |
|
| 1381 |
+ .justify_between() |
|
| 1382 |
+ .gap_3() |
|
| 1383 |
+ .on_click(cx.listener(SideloaderView::manage_accounts)) |
|
| 1384 |
+ .child( |
|
| 1385 |
+ div() |
|
| 1386 |
+ .min_w_0() |
|
| 1387 |
+ .flex() |
|
| 1388 |
+ .flex_1() |
|
| 1389 |
+ .flex_col() |
|
| 1390 |
+ .gap_0p5() |
|
| 1391 |
+ .child( |
|
| 1392 |
+ div() |
|
| 1393 |
+ .text_sm() |
|
| 1394 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 1395 |
+ .text_color(rgb(0x24333a)) |
|
| 1396 |
+ .child("Add an Apple Account"),
|
|
| 1397 |
+ ) |
|
| 1398 |
+ .child( |
|
| 1399 |
+ div() |
|
| 1400 |
+ .text_xs() |
|
| 1401 |
+ .text_color(rgb(0x7b8a90)) |
|
| 1402 |
+ .child("Log in before choosing a developer team."),
|
|
| 1403 |
+ ), |
|
| 1404 |
+ ) |
|
| 1405 |
+ .child( |
|
| 1406 |
+ div() |
|
| 1407 |
+ .flex_none() |
|
| 1408 |
+ .w_7() |
|
| 1409 |
+ .h_7() |
|
| 1410 |
+ .rounded_md() |
|
| 1411 |
+ .bg(rgb(0xebf1f0)) |
|
| 1412 |
+ .text_color(rgb(0x53666d)) |
|
| 1413 |
+ .flex() |
|
| 1414 |
+ .items_center() |
|
| 1415 |
+ .justify_center() |
|
| 1416 |
+ .child(lucide_icon("icons/plus.svg")),
|
|
| 1417 |
+ ) |
|
| 1418 |
+} |
|
| 1419 |
+ |
|
| 1420 |
+fn selected_account_index(accounts: &[AccountOption], preferences: &DeveloperPreferences) -> usize {
|
|
| 1421 |
+ preferences |
|
| 1422 |
+ .account_id |
|
| 1423 |
+ .as_deref() |
|
| 1424 |
+ .and_then(|account_id| {
|
|
| 1425 |
+ accounts |
|
| 1426 |
+ .iter() |
|
| 1427 |
+ .position(|account| account.id.as_ref() == account_id) |
|
| 1428 |
+ }) |
|
| 1429 |
+ .unwrap_or(0) |
|
| 1430 |
+} |
|
| 1431 |
+ |
|
| 1432 |
+fn selected_team_index( |
|
| 1433 |
+ accounts: &[AccountOption], |
|
| 1434 |
+ selected_account: usize, |
|
| 1435 |
+ preferences: &DeveloperPreferences, |
|
| 1436 |
+) -> usize {
|
|
| 1437 |
+ preferences |
|
| 1438 |
+ .team_id |
|
| 1439 |
+ .as_deref() |
|
| 1440 |
+ .and_then(|team_id| {
|
|
| 1441 |
+ accounts |
|
| 1442 |
+ .get(selected_account)? |
|
| 1443 |
+ .teams |
|
| 1444 |
+ .iter() |
|
| 1445 |
+ .position(|team| team.identifier.as_ref() == team_id) |
|
| 1446 |
+ }) |
|
| 1447 |
+ .unwrap_or(0) |
|
| 1448 |
+} |
|
| 1449 |
+ |
|
| 1450 |
+fn selected_app_id_index( |
|
| 1451 |
+ accounts: &[AccountOption], |
|
| 1452 |
+ selected_account: usize, |
|
| 1453 |
+ selected_team: usize, |
|
| 1454 |
+ preferences: &DeveloperPreferences, |
|
| 1455 |
+) -> usize {
|
|
| 1456 |
+ preferences |
|
| 1457 |
+ .app_id |
|
| 1458 |
+ .as_deref() |
|
| 1459 |
+ .and_then(|app_id| {
|
|
| 1460 |
+ accounts |
|
| 1461 |
+ .get(selected_account)? |
|
| 1462 |
+ .teams |
|
| 1463 |
+ .get(selected_team)? |
|
| 1464 |
+ .app_ids |
|
| 1465 |
+ .iter() |
|
| 1466 |
+ .position(|candidate| candidate.identifier.as_ref() == app_id) |
|
| 1467 |
+ }) |
|
| 1468 |
+ .unwrap_or(0) |
|
| 1469 |
+} |
|
| 1470 |
+ |
|
| 1471 |
+fn selected_adi_backend_index( |
|
| 1472 |
+ backends: &[AdiBackendOption], |
|
| 1473 |
+ backend: Option<StoredAdiBackendKind>, |
|
| 1474 |
+) -> usize {
|
|
| 1475 |
+ backend |
|
| 1476 |
+ .and_then(|backend| {
|
|
| 1477 |
+ let backend = AdiBackendKind::from(backend); |
|
| 1478 |
+ backends |
|
| 1479 |
+ .iter() |
|
| 1480 |
+ .position(|candidate| candidate.kind == backend) |
|
| 1481 |
+ }) |
|
| 1482 |
+ .unwrap_or_else(|| adi_services::default_backend(backends)) |
|
| 1483 |
+} |
|
| 1484 |
+ |
|
| 1485 |
+fn ensure_android_adi_identifier(identifier: &mut String) -> bool {
|
|
| 1486 |
+ if identifier.len() == 16 {
|
|
| 1487 |
+ return false; |
|
| 1488 |
+ } |
|
| 1489 |
+ |
|
| 1490 |
+ *identifier = random_android_adi_identifier(); |
|
| 1491 |
+ true |
|
| 1492 |
+} |
|
| 1493 |
+ |
|
| 1494 |
+fn ensure_android_device_uuid(identifier: &mut String) -> bool {
|
|
| 1495 |
+ if is_uuid(identifier) {
|
|
| 1496 |
+ return false; |
|
| 1497 |
+ } |
|
| 1498 |
+ |
|
| 1499 |
+ *identifier = uuid::Uuid::new_v4().hyphenated().to_string().to_uppercase(); |
|
| 1500 |
+ true |
|
| 1501 |
+} |
|
| 1502 |
+ |
|
| 1503 |
+fn is_uuid(identifier: &str) -> bool {
|
|
| 1504 |
+ uuid::Uuid::parse_str(identifier).is_ok() |
|
| 1505 |
+} |
|
| 1506 |
+ |
|
| 1507 |
+fn android_device_identity_from_host( |
|
| 1508 |
+ host_identity: &MachineIdentity, |
|
| 1509 |
+ device_uuid: String, |
|
| 1510 |
+) -> MachineIdentity {
|
|
| 1511 |
+ MachineIdentity {
|
|
| 1512 |
+ machine_name: host_identity.machine_name.clone(), |
|
| 1513 |
+ os_name: host_identity.os_name.clone(), |
|
| 1514 |
+ os_version: host_identity.os_version.clone(), |
|
| 1515 |
+ machine_id: device_uuid.into(), |
|
| 1516 |
+ } |
|
| 1517 |
+} |
|
| 1518 |
+ |
|
| 1519 |
+fn apply_machine_identity_preferences( |
|
| 1520 |
+ identity: &mut MachineIdentity, |
|
| 1521 |
+ preferences: &MachineIdentityPreferences, |
|
| 1522 |
+) {
|
|
| 1523 |
+ if let Some(machine_name) = preferences.machine_name.as_ref() {
|
|
| 1524 |
+ identity.machine_name = machine_name.clone().into(); |
|
| 1525 |
+ } |
|
| 1526 |
+ if let Some(os_name) = preferences.os_name.as_ref() {
|
|
| 1527 |
+ identity.os_name = os_name.clone().into(); |
|
| 1528 |
+ } |
|
| 1529 |
+ if let Some(os_version) = preferences.os_version.as_ref() {
|
|
| 1530 |
+ identity.os_version = os_version.clone().into(); |
|
| 1531 |
+ } |
|
| 1532 |
+ if let Some(machine_id) = preferences.machine_id.as_ref() {
|
|
| 1533 |
+ identity.machine_id = machine_id.clone().into(); |
|
| 1534 |
+ } |
|
| 1535 |
+} |
|
| 1536 |
+ |
|
| 1537 |
+fn random_android_adi_identifier() -> String {
|
|
| 1538 |
+ const HEX: &[u8; 16] = b"0123456789abcdef"; |
|
| 1539 |
+ let mut rng = rand::rng(); |
|
| 1540 |
+ let mut id = String::with_capacity(16); |
|
| 1541 |
+ for _ in 0..16 {
|
|
| 1542 |
+ let index: usize = rng.random_range(0..HEX.len()); |
|
| 1543 |
+ id.push(HEX[index] as char); |
|
| 1544 |
+ } |
|
| 1545 |
+ id |
|
| 1546 |
+} |
|
| 1547 |
+ |
|
| 1548 |
+fn first_ipa_path(paths: &ExternalPaths) -> Option<PathBuf> {
|
|
| 1549 |
+ paths.paths().iter().find(|path| is_ipa_path(path)).cloned() |
|
| 1550 |
+} |
|
| 1551 |
+ |
|
| 1552 |
+fn paths_include_ipa(paths: &ExternalPaths) -> bool {
|
|
| 1553 |
+ first_ipa_path(paths).is_some() |
|
| 1554 |
+} |
|
| 1555 |
+ |
|
| 829 | 1556 |
impl Render for SideloaderView {
|
| 830 | 1557 |
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
| 831 |
- if self.sideload_operation.is_busy() {
|
|
| 1558 |
+ if self.sideload_operation.is_busy() || self.device_selection.is_refreshing() {
|
|
| 832 | 1559 |
self.spinner_turns = (self.spinner_turns + 0.035) % 1.; |
| 833 | 1560 |
window.request_animation_frame(); |
| 834 | 1561 |
} else {
|
| ... | ... |
@@ -843,6 +1570,12 @@ impl Render for SideloaderView {
|
| 843 | 1570 |
.bg(rgb(0xf4f6f4)) |
| 844 | 1571 |
.text_color(rgb(0x263238)) |
| 845 | 1572 |
.font_family(".SystemUIFont")
|
| 1573 |
+ .can_drop(|dragged, _, _| {
|
|
| 1574 |
+ dragged |
|
| 1575 |
+ .downcast_ref::<ExternalPaths>() |
|
| 1576 |
+ .is_some_and(paths_include_ipa) |
|
| 1577 |
+ }) |
|
| 1578 |
+ .on_drop(cx.listener(Self::drop_ipa)) |
|
| 846 | 1579 |
.child( |
| 847 | 1580 |
div() |
| 848 | 1581 |
.flex() |
| ... | ... |
@@ -3,7 +3,6 @@ use gpui::SharedString; |
| 3 | 3 |
#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
| 4 | 4 |
pub(crate) enum PickerId {
|
| 5 | 5 |
Account, |
| 6 |
- App, |
|
| 7 | 6 |
Device, |
| 8 | 7 |
} |
| 9 | 8 |
|
| ... | ... |
@@ -24,6 +23,7 @@ pub(crate) struct TeamOption {
|
| 24 | 23 |
|
| 25 | 24 |
#[derive(Clone, Debug)] |
| 26 | 25 |
pub(crate) struct AccountOption {
|
| 26 |
+ pub(crate) id: SharedString, |
|
| 27 | 27 |
pub(crate) label: SharedString, |
| 28 | 28 |
pub(crate) apple_id: SharedString, |
| 29 | 29 |
pub(crate) detail: SharedString, |
| ... | ... |
@@ -37,16 +37,532 @@ pub(crate) struct PatchOption {
|
| 37 | 37 |
pub(crate) detail: SharedString, |
| 38 | 38 |
} |
| 39 | 39 |
|
| 40 |
+#[derive(Clone, Debug)] |
|
| 41 |
+pub(crate) struct AppFieldValue {
|
|
| 42 |
+ pub(crate) default: SharedString, |
|
| 43 |
+ pub(crate) override_value: Option<SharedString>, |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+impl AppFieldValue {
|
|
| 47 |
+ pub(crate) fn new(default: impl Into<SharedString>) -> Self {
|
|
| 48 |
+ Self {
|
|
| 49 |
+ default: default.into(), |
|
| 50 |
+ override_value: None, |
|
| 51 |
+ } |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ pub(crate) fn value(&self) -> &SharedString {
|
|
| 55 |
+ self.override_value.as_ref().unwrap_or(&self.default) |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ pub(crate) fn is_overridden(&self) -> bool {
|
|
| 59 |
+ self.override_value.is_some() |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ pub(crate) fn set_override(&mut self, value: String) {
|
|
| 63 |
+ if value == self.default.as_ref() {
|
|
| 64 |
+ self.override_value = None; |
|
| 65 |
+ } else {
|
|
| 66 |
+ self.override_value = Some(value.into()); |
|
| 67 |
+ } |
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+ pub(crate) fn clear_override(&mut self) {
|
|
| 71 |
+ self.override_value = None; |
|
| 72 |
+ } |
|
| 73 |
+} |
|
| 74 |
+ |
|
| 75 |
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] |
|
| 76 |
+pub(crate) enum SupportedDeviceFamily {
|
|
| 77 |
+ IPhone, |
|
| 78 |
+ IPad, |
|
| 79 |
+ AppleTv, |
|
| 80 |
+ AppleWatch, |
|
| 81 |
+ Mac, |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+impl SupportedDeviceFamily {
|
|
| 85 |
+ pub(crate) const ALL: [Self; 5] = [ |
|
| 86 |
+ Self::IPhone, |
|
| 87 |
+ Self::IPad, |
|
| 88 |
+ Self::AppleTv, |
|
| 89 |
+ Self::AppleWatch, |
|
| 90 |
+ Self::Mac, |
|
| 91 |
+ ]; |
|
| 92 |
+ |
|
| 93 |
+ pub(crate) fn label(self) -> &'static str {
|
|
| 94 |
+ match self {
|
|
| 95 |
+ Self::IPhone => "iPhone", |
|
| 96 |
+ Self::IPad => "iPad", |
|
| 97 |
+ Self::AppleTv => "Apple TV", |
|
| 98 |
+ Self::AppleWatch => "Apple Watch", |
|
| 99 |
+ Self::Mac => "Mac", |
|
| 100 |
+ } |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ pub(crate) fn from_device_family_id(id: u64) -> Option<Self> {
|
|
| 104 |
+ match id {
|
|
| 105 |
+ 1 => Some(Self::IPhone), |
|
| 106 |
+ 2 => Some(Self::IPad), |
|
| 107 |
+ 3 => Some(Self::AppleTv), |
|
| 108 |
+ 4 => Some(Self::AppleWatch), |
|
| 109 |
+ 6 => Some(Self::Mac), |
|
| 110 |
+ _ => None, |
|
| 111 |
+ } |
|
| 112 |
+ } |
|
| 113 |
+ |
|
| 114 |
+ pub(crate) fn parse_list(value: &str) -> Vec<Self> {
|
|
| 115 |
+ let mut families = value |
|
| 116 |
+ .split(',')
|
|
| 117 |
+ .filter_map(|value| Self::from_label(value.trim())) |
|
| 118 |
+ .collect::<Vec<_>>(); |
|
| 119 |
+ normalize_supported_families(&mut families); |
|
| 120 |
+ families |
|
| 121 |
+ } |
|
| 122 |
+ |
|
| 123 |
+ pub(crate) fn display_list(families: &[Self]) -> SharedString {
|
|
| 124 |
+ if families.is_empty() {
|
|
| 125 |
+ return "Unknown".into(); |
|
| 126 |
+ } |
|
| 127 |
+ |
|
| 128 |
+ families |
|
| 129 |
+ .iter() |
|
| 130 |
+ .map(|family| family.label()) |
|
| 131 |
+ .collect::<Vec<_>>() |
|
| 132 |
+ .join(", ")
|
|
| 133 |
+ .into() |
|
| 134 |
+ } |
|
| 135 |
+ |
|
| 136 |
+ fn from_label(value: &str) -> Option<Self> {
|
|
| 137 |
+ match value.to_ascii_lowercase().as_str() {
|
|
| 138 |
+ "iphone" | "ipod" | "ios" => Some(Self::IPhone), |
|
| 139 |
+ "ipad" | "ipados" => Some(Self::IPad), |
|
| 140 |
+ "apple tv" | "appletv" | "tvos" => Some(Self::AppleTv), |
|
| 141 |
+ "apple watch" | "applewatch" | "watch" | "watchos" => Some(Self::AppleWatch), |
|
| 142 |
+ "mac" | "macos" => Some(Self::Mac), |
|
| 143 |
+ _ => None, |
|
| 144 |
+ } |
|
| 145 |
+ } |
|
| 146 |
+} |
|
| 147 |
+ |
|
| 148 |
+#[derive(Clone, Debug)] |
|
| 149 |
+pub(crate) struct SupportedDevicesValue {
|
|
| 150 |
+ pub(crate) default: Vec<SupportedDeviceFamily>, |
|
| 151 |
+ pub(crate) override_value: Option<Vec<SupportedDeviceFamily>>, |
|
| 152 |
+} |
|
| 153 |
+ |
|
| 154 |
+impl SupportedDevicesValue {
|
|
| 155 |
+ pub(crate) fn new(default: Vec<SupportedDeviceFamily>) -> Self {
|
|
| 156 |
+ Self {
|
|
| 157 |
+ default, |
|
| 158 |
+ override_value: None, |
|
| 159 |
+ } |
|
| 160 |
+ } |
|
| 161 |
+ |
|
| 162 |
+ pub(crate) fn value(&self) -> &[SupportedDeviceFamily] {
|
|
| 163 |
+ self.override_value.as_deref().unwrap_or(&self.default) |
|
| 164 |
+ } |
|
| 165 |
+ |
|
| 166 |
+ pub(crate) fn display_value(&self) -> SharedString {
|
|
| 167 |
+ SupportedDeviceFamily::display_list(self.value()) |
|
| 168 |
+ } |
|
| 169 |
+ |
|
| 170 |
+ pub(crate) fn is_overridden(&self) -> bool {
|
|
| 171 |
+ self.override_value.is_some() |
|
| 172 |
+ } |
|
| 173 |
+ |
|
| 174 |
+ pub(crate) fn set_override(&mut self, mut value: Vec<SupportedDeviceFamily>) {
|
|
| 175 |
+ normalize_supported_families(&mut value); |
|
| 176 |
+ if value == self.default {
|
|
| 177 |
+ self.override_value = None; |
|
| 178 |
+ } else {
|
|
| 179 |
+ self.override_value = Some(value); |
|
| 180 |
+ } |
|
| 181 |
+ } |
|
| 182 |
+ |
|
| 183 |
+ pub(crate) fn clear_override(&mut self) {
|
|
| 184 |
+ self.override_value = None; |
|
| 185 |
+ } |
|
| 186 |
+} |
|
| 187 |
+ |
|
| 188 |
+fn normalize_supported_families(families: &mut Vec<SupportedDeviceFamily>) {
|
|
| 189 |
+ families.sort(); |
|
| 190 |
+ families.dedup(); |
|
| 191 |
+} |
|
| 192 |
+ |
|
| 193 |
+#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
|
| 194 |
+pub(crate) enum AppMetadataField {
|
|
| 195 |
+ Name, |
|
| 196 |
+ BundleId, |
|
| 197 |
+ Version, |
|
| 198 |
+ Build, |
|
| 199 |
+ Executable, |
|
| 200 |
+ MinimumOs, |
|
| 201 |
+ SupportedDevices, |
|
| 202 |
+} |
|
| 203 |
+ |
|
| 204 |
+impl AppMetadataField {
|
|
| 205 |
+ pub(crate) fn label(self) -> &'static str {
|
|
| 206 |
+ match self {
|
|
| 207 |
+ Self::Name => "Name", |
|
| 208 |
+ Self::BundleId => "Bundle ID", |
|
| 209 |
+ Self::Version => "Version", |
|
| 210 |
+ Self::Build => "Build", |
|
| 211 |
+ Self::Executable => "Executable", |
|
| 212 |
+ Self::MinimumOs => "Minimum OS", |
|
| 213 |
+ Self::SupportedDevices => "Supported devices", |
|
| 214 |
+ } |
|
| 215 |
+ } |
|
| 216 |
+} |
|
| 217 |
+ |
|
| 218 |
+#[derive(Clone, Debug)] |
|
| 219 |
+pub(crate) struct AppMetadata {
|
|
| 220 |
+ pub(crate) name: AppFieldValue, |
|
| 221 |
+ pub(crate) bundle_id: AppFieldValue, |
|
| 222 |
+ pub(crate) version: AppFieldValue, |
|
| 223 |
+ pub(crate) build: AppFieldValue, |
|
| 224 |
+ pub(crate) executable: AppFieldValue, |
|
| 225 |
+ pub(crate) minimum_os: AppFieldValue, |
|
| 226 |
+ pub(crate) supported_devices: SupportedDevicesValue, |
|
| 227 |
+} |
|
| 228 |
+ |
|
| 229 |
+impl AppMetadata {
|
|
| 230 |
+ pub(crate) fn sample( |
|
| 231 |
+ name: impl Into<SharedString>, |
|
| 232 |
+ bundle_id: impl Into<SharedString>, |
|
| 233 |
+ version: impl Into<SharedString>, |
|
| 234 |
+ build: impl Into<SharedString>, |
|
| 235 |
+ executable: impl Into<SharedString>, |
|
| 236 |
+ minimum_os: impl Into<SharedString>, |
|
| 237 |
+ supported_devices: Vec<SupportedDeviceFamily>, |
|
| 238 |
+ ) -> Self {
|
|
| 239 |
+ Self {
|
|
| 240 |
+ name: AppFieldValue::new(name), |
|
| 241 |
+ bundle_id: AppFieldValue::new(bundle_id), |
|
| 242 |
+ version: AppFieldValue::new(version), |
|
| 243 |
+ build: AppFieldValue::new(build), |
|
| 244 |
+ executable: AppFieldValue::new(executable), |
|
| 245 |
+ minimum_os: AppFieldValue::new(minimum_os), |
|
| 246 |
+ supported_devices: SupportedDevicesValue::new(supported_devices), |
|
| 247 |
+ } |
|
| 248 |
+ } |
|
| 249 |
+ |
|
| 250 |
+ pub(crate) fn field(&self, field: AppMetadataField) -> &AppFieldValue {
|
|
| 251 |
+ match field {
|
|
| 252 |
+ AppMetadataField::Name => &self.name, |
|
| 253 |
+ AppMetadataField::BundleId => &self.bundle_id, |
|
| 254 |
+ AppMetadataField::Version => &self.version, |
|
| 255 |
+ AppMetadataField::Build => &self.build, |
|
| 256 |
+ AppMetadataField::Executable => &self.executable, |
|
| 257 |
+ AppMetadataField::MinimumOs => &self.minimum_os, |
|
| 258 |
+ AppMetadataField::SupportedDevices => {
|
|
| 259 |
+ panic!("supported devices are not represented as a text metadata field")
|
|
| 260 |
+ } |
|
| 261 |
+ } |
|
| 262 |
+ } |
|
| 263 |
+ |
|
| 264 |
+ pub(crate) fn field_mut(&mut self, field: AppMetadataField) -> &mut AppFieldValue {
|
|
| 265 |
+ match field {
|
|
| 266 |
+ AppMetadataField::Name => &mut self.name, |
|
| 267 |
+ AppMetadataField::BundleId => &mut self.bundle_id, |
|
| 268 |
+ AppMetadataField::Version => &mut self.version, |
|
| 269 |
+ AppMetadataField::Build => &mut self.build, |
|
| 270 |
+ AppMetadataField::Executable => &mut self.executable, |
|
| 271 |
+ AppMetadataField::MinimumOs => &mut self.minimum_os, |
|
| 272 |
+ AppMetadataField::SupportedDevices => {
|
|
| 273 |
+ panic!("supported devices are not represented as a text metadata field")
|
|
| 274 |
+ } |
|
| 275 |
+ } |
|
| 276 |
+ } |
|
| 277 |
+} |
|
| 278 |
+ |
|
| 279 |
+#[derive(Clone, Debug, PartialEq)] |
|
| 280 |
+pub(crate) struct AppEntitlement {
|
|
| 281 |
+ pub(crate) key: SharedString, |
|
| 282 |
+ pub(crate) value: EntitlementValue, |
|
| 283 |
+} |
|
| 284 |
+ |
|
| 285 |
+#[derive(Clone, Debug, PartialEq)] |
|
| 286 |
+pub(crate) enum EntitlementValue {
|
|
| 287 |
+ String(SharedString), |
|
| 288 |
+ Boolean(bool), |
|
| 289 |
+ Integer(i64), |
|
| 290 |
+ Number(f64), |
|
| 291 |
+ Array(Vec<EntitlementValue>), |
|
| 292 |
+ Dictionary(Vec<(SharedString, EntitlementValue)>), |
|
| 293 |
+ Data(Vec<u8>), |
|
| 294 |
+ Date(SharedString), |
|
| 295 |
+ Uid(u64), |
|
| 296 |
+ Unknown(SharedString), |
|
| 297 |
+} |
|
| 298 |
+ |
|
| 299 |
+impl EntitlementValue {
|
|
| 300 |
+ pub(crate) const EDITABLE_TYPE_LABELS: [&'static str; 3] = ["String", "Boolean", "Array"]; |
|
| 301 |
+ |
|
| 302 |
+ pub(crate) fn type_label(&self) -> &'static str {
|
|
| 303 |
+ match self {
|
|
| 304 |
+ Self::String(_) => "String", |
|
| 305 |
+ Self::Boolean(_) => "Boolean", |
|
| 306 |
+ Self::Integer(_) => "Integer", |
|
| 307 |
+ Self::Number(_) => "Number", |
|
| 308 |
+ Self::Array(_) => "Array", |
|
| 309 |
+ Self::Dictionary(_) => "Dictionary", |
|
| 310 |
+ Self::Data(_) => "Data", |
|
| 311 |
+ Self::Date(_) => "Date", |
|
| 312 |
+ Self::Uid(_) => "UID", |
|
| 313 |
+ Self::Unknown(_) => "Value", |
|
| 314 |
+ } |
|
| 315 |
+ } |
|
| 316 |
+ |
|
| 317 |
+ pub(crate) fn display_text(&self) -> SharedString {
|
|
| 318 |
+ self.edit_text().into() |
|
| 319 |
+ } |
|
| 320 |
+ |
|
| 321 |
+ pub(crate) fn edit_text(&self) -> String {
|
|
| 322 |
+ match self {
|
|
| 323 |
+ Self::String(value) => value.to_string(), |
|
| 324 |
+ Self::Boolean(value) => value.to_string(), |
|
| 325 |
+ Self::Integer(value) => value.to_string(), |
|
| 326 |
+ Self::Number(value) => value.to_string(), |
|
| 327 |
+ Self::Array(values) => values |
|
| 328 |
+ .iter() |
|
| 329 |
+ .map(Self::compact_text) |
|
| 330 |
+ .collect::<Vec<_>>() |
|
| 331 |
+ .join(", "),
|
|
| 332 |
+ Self::Dictionary(values) => format!("{} keys", values.len()),
|
|
| 333 |
+ Self::Data(value) => format!("{} bytes", value.len()),
|
|
| 334 |
+ Self::Date(value) => value.to_string(), |
|
| 335 |
+ Self::Uid(value) => value.to_string(), |
|
| 336 |
+ Self::Unknown(value) => value.to_string(), |
|
| 337 |
+ } |
|
| 338 |
+ } |
|
| 339 |
+ |
|
| 340 |
+ pub(crate) fn with_type_label(&self, label: &str) -> Self {
|
|
| 341 |
+ Self::from_type_and_text(label, &self.edit_text()) |
|
| 342 |
+ } |
|
| 343 |
+ |
|
| 344 |
+ pub(crate) fn with_edit_text(&self, text: &str) -> Self {
|
|
| 345 |
+ Self::from_type_and_text(self.type_label(), text) |
|
| 346 |
+ } |
|
| 347 |
+ |
|
| 348 |
+ pub(crate) fn string_array(values: Vec<String>) -> Self {
|
|
| 349 |
+ Self::Array( |
|
| 350 |
+ values |
|
| 351 |
+ .into_iter() |
|
| 352 |
+ .map(|value| Self::String(value.into())) |
|
| 353 |
+ .collect(), |
|
| 354 |
+ ) |
|
| 355 |
+ } |
|
| 356 |
+ |
|
| 357 |
+ pub(crate) fn array_edit_values(&self) -> Vec<String> {
|
|
| 358 |
+ match self {
|
|
| 359 |
+ Self::Array(values) => values.iter().map(Self::edit_text).collect(), |
|
| 360 |
+ _ => vec![self.edit_text()], |
|
| 361 |
+ } |
|
| 362 |
+ } |
|
| 363 |
+ |
|
| 364 |
+ pub(crate) fn from_type_and_text(label: &str, text: &str) -> Self {
|
|
| 365 |
+ match label {
|
|
| 366 |
+ "String" => Self::String(text.to_string().into()), |
|
| 367 |
+ "Boolean" => Self::Boolean(matches!( |
|
| 368 |
+ text.trim().to_ascii_lowercase().as_str(), |
|
| 369 |
+ "true" | "yes" | "1" |
|
| 370 |
+ )), |
|
| 371 |
+ "Integer" => Self::Integer(text.trim().parse().unwrap_or_default()), |
|
| 372 |
+ "Number" => Self::Number(text.trim().parse().unwrap_or_default()), |
|
| 373 |
+ "Array" => Self::Array( |
|
| 374 |
+ text.split(',')
|
|
| 375 |
+ .map(str::trim) |
|
| 376 |
+ .filter(|value| !value.is_empty()) |
|
| 377 |
+ .map(|value| Self::String(value.to_string().into())) |
|
| 378 |
+ .collect(), |
|
| 379 |
+ ), |
|
| 380 |
+ "Dictionary" => Self::Dictionary(Vec::new()), |
|
| 381 |
+ "Data" => Self::Data(Vec::new()), |
|
| 382 |
+ "Date" => Self::Date(text.to_string().into()), |
|
| 383 |
+ "UID" => Self::Uid(text.trim().parse().unwrap_or_default()), |
|
| 384 |
+ _ => Self::Unknown(text.to_string().into()), |
|
| 385 |
+ } |
|
| 386 |
+ } |
|
| 387 |
+ |
|
| 388 |
+ fn compact_text(&self) -> String {
|
|
| 389 |
+ match self {
|
|
| 390 |
+ Self::Array(values) => format!("{} items", values.len()),
|
|
| 391 |
+ Self::Dictionary(values) => format!("{} keys", values.len()),
|
|
| 392 |
+ Self::Data(value) => format!("{} bytes", value.len()),
|
|
| 393 |
+ _ => self.edit_text(), |
|
| 394 |
+ } |
|
| 395 |
+ } |
|
| 396 |
+} |
|
| 397 |
+ |
|
| 40 | 398 |
#[derive(Clone, Debug)] |
| 41 | 399 |
pub(crate) struct AppOption {
|
| 42 |
- pub(crate) name: SharedString, |
|
| 43 |
- pub(crate) bundle_id: SharedString, |
|
| 44 |
- pub(crate) version: SharedString, |
|
| 45 |
- pub(crate) build: SharedString, |
|
| 400 |
+ pub(crate) metadata: AppMetadata, |
|
| 46 | 401 |
pub(crate) path: SharedString, |
| 402 |
+ pub(crate) icon_path: Option<SharedString>, |
|
| 403 |
+ pub(crate) icon_override_path: Option<SharedString>, |
|
| 404 |
+ pub(crate) entitlements: Vec<AppEntitlement>, |
|
| 405 |
+ pub(crate) entitlement_overrides: Option<Vec<AppEntitlement>>, |
|
| 47 | 406 |
pub(crate) patches: Vec<PatchOption>, |
| 48 | 407 |
} |
| 49 | 408 |
|
| 409 |
+impl AppOption {
|
|
| 410 |
+ pub(crate) fn name(&self) -> &SharedString {
|
|
| 411 |
+ self.metadata.name.value() |
|
| 412 |
+ } |
|
| 413 |
+ |
|
| 414 |
+ pub(crate) fn bundle_id(&self) -> &SharedString {
|
|
| 415 |
+ self.metadata.bundle_id.value() |
|
| 416 |
+ } |
|
| 417 |
+ |
|
| 418 |
+ pub(crate) fn version(&self) -> &SharedString {
|
|
| 419 |
+ self.metadata.version.value() |
|
| 420 |
+ } |
|
| 421 |
+ |
|
| 422 |
+ pub(crate) fn build(&self) -> &SharedString {
|
|
| 423 |
+ self.metadata.build.value() |
|
| 424 |
+ } |
|
| 425 |
+ |
|
| 426 |
+ pub(crate) fn field(&self, field: AppMetadataField) -> &AppFieldValue {
|
|
| 427 |
+ self.metadata.field(field) |
|
| 428 |
+ } |
|
| 429 |
+ |
|
| 430 |
+ pub(crate) fn field_display_value(&self, field: AppMetadataField) -> SharedString {
|
|
| 431 |
+ match field {
|
|
| 432 |
+ AppMetadataField::SupportedDevices => self.metadata.supported_devices.display_value(), |
|
| 433 |
+ field => self.field(field).value().clone(), |
|
| 434 |
+ } |
|
| 435 |
+ } |
|
| 436 |
+ |
|
| 437 |
+ pub(crate) fn field_is_overridden(&self, field: AppMetadataField) -> bool {
|
|
| 438 |
+ match field {
|
|
| 439 |
+ AppMetadataField::SupportedDevices => self.metadata.supported_devices.is_overridden(), |
|
| 440 |
+ field => self.field(field).is_overridden(), |
|
| 441 |
+ } |
|
| 442 |
+ } |
|
| 443 |
+ |
|
| 444 |
+ pub(crate) fn set_field_override(&mut self, field: AppMetadataField, value: String) {
|
|
| 445 |
+ match field {
|
|
| 446 |
+ AppMetadataField::SupportedDevices => self |
|
| 447 |
+ .metadata |
|
| 448 |
+ .supported_devices |
|
| 449 |
+ .set_override(SupportedDeviceFamily::parse_list(&value)), |
|
| 450 |
+ field => self.metadata.field_mut(field).set_override(value), |
|
| 451 |
+ } |
|
| 452 |
+ } |
|
| 453 |
+ |
|
| 454 |
+ pub(crate) fn set_supported_devices_override(&mut self, value: Vec<SupportedDeviceFamily>) {
|
|
| 455 |
+ self.metadata.supported_devices.set_override(value); |
|
| 456 |
+ } |
|
| 457 |
+ |
|
| 458 |
+ pub(crate) fn clear_field_override(&mut self, field: AppMetadataField) {
|
|
| 459 |
+ match field {
|
|
| 460 |
+ AppMetadataField::SupportedDevices => self.metadata.supported_devices.clear_override(), |
|
| 461 |
+ field => self.metadata.field_mut(field).clear_override(), |
|
| 462 |
+ } |
|
| 463 |
+ } |
|
| 464 |
+ |
|
| 465 |
+ pub(crate) fn supported_devices(&self) -> &[SupportedDeviceFamily] {
|
|
| 466 |
+ self.metadata.supported_devices.value() |
|
| 467 |
+ } |
|
| 468 |
+ |
|
| 469 |
+ pub(crate) fn displayed_icon_path(&self) -> Option<&SharedString> {
|
|
| 470 |
+ self.icon_override_path.as_ref().or(self.icon_path.as_ref()) |
|
| 471 |
+ } |
|
| 472 |
+ |
|
| 473 |
+ pub(crate) fn effective_entitlements(&self, team_id: &SharedString) -> Vec<AppEntitlement> {
|
|
| 474 |
+ if let Some(overrides) = self.entitlement_overrides.as_ref() {
|
|
| 475 |
+ return overrides.clone(); |
|
| 476 |
+ } |
|
| 477 |
+ |
|
| 478 |
+ self.default_effective_entitlements(team_id) |
|
| 479 |
+ } |
|
| 480 |
+ |
|
| 481 |
+ pub(crate) fn default_effective_entitlements( |
|
| 482 |
+ &self, |
|
| 483 |
+ team_id: &SharedString, |
|
| 484 |
+ ) -> Vec<AppEntitlement> {
|
|
| 485 |
+ self.entitlements |
|
| 486 |
+ .iter() |
|
| 487 |
+ .map(|entitlement| team_adjusted_entitlement(entitlement, self.bundle_id(), team_id)) |
|
| 488 |
+ .collect() |
|
| 489 |
+ } |
|
| 490 |
+ |
|
| 491 |
+ pub(crate) fn entitlements_are_overridden(&self) -> bool {
|
|
| 492 |
+ self.entitlement_overrides.is_some() |
|
| 493 |
+ } |
|
| 494 |
+} |
|
| 495 |
+ |
|
| 496 |
+fn team_adjusted_entitlement( |
|
| 497 |
+ entitlement: &AppEntitlement, |
|
| 498 |
+ bundle_id: &SharedString, |
|
| 499 |
+ team_id: &SharedString, |
|
| 500 |
+) -> AppEntitlement {
|
|
| 501 |
+ let mut entitlement = entitlement.clone(); |
|
| 502 |
+ match entitlement.key.as_ref() {
|
|
| 503 |
+ "application-identifier" => {
|
|
| 504 |
+ entitlement.value = EntitlementValue::String(team_prefixed_identifier( |
|
| 505 |
+ &entitlement.value.edit_text(), |
|
| 506 |
+ bundle_id, |
|
| 507 |
+ team_id, |
|
| 508 |
+ )); |
|
| 509 |
+ } |
|
| 510 |
+ "com.apple.developer.team-identifier" => {
|
|
| 511 |
+ entitlement.value = EntitlementValue::String(team_id.clone()); |
|
| 512 |
+ } |
|
| 513 |
+ "keychain-access-groups" => {
|
|
| 514 |
+ entitlement.value = |
|
| 515 |
+ team_prefixed_keychain_groups(&entitlement.value, bundle_id, team_id); |
|
| 516 |
+ } |
|
| 517 |
+ _ => {}
|
|
| 518 |
+ } |
|
| 519 |
+ entitlement |
|
| 520 |
+} |
|
| 521 |
+ |
|
| 522 |
+fn team_prefixed_keychain_groups( |
|
| 523 |
+ value: &EntitlementValue, |
|
| 524 |
+ bundle_id: &SharedString, |
|
| 525 |
+ team_id: &SharedString, |
|
| 526 |
+) -> EntitlementValue {
|
|
| 527 |
+ match value {
|
|
| 528 |
+ EntitlementValue::Array(values) => EntitlementValue::Array( |
|
| 529 |
+ values |
|
| 530 |
+ .iter() |
|
| 531 |
+ .map(|value| match value {
|
|
| 532 |
+ EntitlementValue::String(value) => EntitlementValue::String( |
|
| 533 |
+ team_prefixed_identifier(value.as_ref(), bundle_id, team_id), |
|
| 534 |
+ ), |
|
| 535 |
+ value => value.clone(), |
|
| 536 |
+ }) |
|
| 537 |
+ .collect(), |
|
| 538 |
+ ), |
|
| 539 |
+ value => EntitlementValue::Array( |
|
| 540 |
+ value |
|
| 541 |
+ .edit_text() |
|
| 542 |
+ .split(',')
|
|
| 543 |
+ .map(str::trim) |
|
| 544 |
+ .filter(|value| !value.is_empty()) |
|
| 545 |
+ .map(|value| {
|
|
| 546 |
+ EntitlementValue::String(team_prefixed_identifier(value, bundle_id, team_id)) |
|
| 547 |
+ }) |
|
| 548 |
+ .collect(), |
|
| 549 |
+ ), |
|
| 550 |
+ } |
|
| 551 |
+} |
|
| 552 |
+ |
|
| 553 |
+fn team_prefixed_identifier( |
|
| 554 |
+ value: &str, |
|
| 555 |
+ bundle_id: &SharedString, |
|
| 556 |
+ team_id: &SharedString, |
|
| 557 |
+) -> SharedString {
|
|
| 558 |
+ let suffix = value |
|
| 559 |
+ .split_once('.')
|
|
| 560 |
+ .map(|(_, suffix)| suffix) |
|
| 561 |
+ .filter(|suffix| !suffix.is_empty()) |
|
| 562 |
+ .unwrap_or(bundle_id.as_ref()); |
|
| 563 |
+ format!("{team_id}.{suffix}").into()
|
|
| 564 |
+} |
|
| 565 |
+ |
|
| 50 | 566 |
#[derive(Clone, Debug)] |
| 51 | 567 |
pub(crate) struct DeviceOption {
|
| 52 | 568 |
pub(crate) name: SharedString, |
| ... | ... |
@@ -56,14 +572,100 @@ pub(crate) struct DeviceOption {
|
| 56 | 572 |
pub(crate) connection: SharedString, |
| 57 | 573 |
} |
| 58 | 574 |
|
| 575 |
+#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
|
| 576 |
+pub(crate) enum AdiBackendKind {
|
|
| 577 |
+ SystemAdid, |
|
| 578 |
+ WindowsCoreAdi, |
|
| 579 |
+ AndroidCoreAdi, |
|
| 580 |
+} |
|
| 581 |
+ |
|
| 582 |
+#[derive(Clone, Debug)] |
|
| 583 |
+pub(crate) struct AdiBackendDetail {
|
|
| 584 |
+ pub(crate) label: String, |
|
| 585 |
+ pub(crate) value: String, |
|
| 586 |
+} |
|
| 587 |
+ |
|
| 588 |
+#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
|
| 589 |
+pub(crate) enum AdiBackendAvailability {
|
|
| 590 |
+ Ready, |
|
| 591 |
+ NeedsSetup, |
|
| 592 |
+ Unavailable, |
|
| 593 |
+} |
|
| 594 |
+ |
|
| 595 |
+impl AdiBackendAvailability {
|
|
| 596 |
+ pub(crate) fn label(self) -> String {
|
|
| 597 |
+ match self {
|
|
| 598 |
+ Self::Ready => "Ready", |
|
| 599 |
+ Self::NeedsSetup => "Needs setup", |
|
| 600 |
+ Self::Unavailable => "Unavailable", |
|
| 601 |
+ } |
|
| 602 |
+ .to_string() |
|
| 603 |
+ } |
|
| 604 |
+ |
|
| 605 |
+ pub(crate) fn is_ready(self) -> bool {
|
|
| 606 |
+ self == Self::Ready |
|
| 607 |
+ } |
|
| 608 |
+ |
|
| 609 |
+ pub(crate) fn is_available(self) -> bool {
|
|
| 610 |
+ self != Self::Unavailable |
|
| 611 |
+ } |
|
| 612 |
+} |
|
| 613 |
+ |
|
| 614 |
+#[derive(Clone, Debug)] |
|
| 615 |
+pub(crate) enum AdiProvisioningState {
|
|
| 616 |
+ Unknown, |
|
| 617 |
+ NotAvailable, |
|
| 618 |
+ Provisioned, |
|
| 619 |
+ NotProvisioned, |
|
| 620 |
+ Error(String), |
|
| 621 |
+} |
|
| 622 |
+ |
|
| 623 |
+impl AdiProvisioningState {
|
|
| 624 |
+ pub(crate) fn label(&self) -> String {
|
|
| 625 |
+ match self {
|
|
| 626 |
+ Self::Unknown => "Unknown", |
|
| 627 |
+ Self::NotAvailable => "Not available", |
|
| 628 |
+ Self::Provisioned => "Provisioned", |
|
| 629 |
+ Self::NotProvisioned => "Not provisioned", |
|
| 630 |
+ Self::Error(_) => "Failed", |
|
| 631 |
+ } |
|
| 632 |
+ .to_string() |
|
| 633 |
+ } |
|
| 634 |
+ |
|
| 635 |
+ pub(crate) fn detail(&self) -> Option<String> {
|
|
| 636 |
+ match self {
|
|
| 637 |
+ Self::Error(error) => Some(error.clone()), |
|
| 638 |
+ _ => None, |
|
| 639 |
+ } |
|
| 640 |
+ } |
|
| 641 |
+} |
|
| 642 |
+ |
|
| 643 |
+#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
|
| 644 |
+pub(crate) enum AdiRepairAction {
|
|
| 645 |
+ InstallCoreAdi, |
|
| 646 |
+ LocateLibrary, |
|
| 647 |
+} |
|
| 648 |
+ |
|
| 649 |
+impl AdiRepairAction {
|
|
| 650 |
+ pub(crate) fn label(self) -> String {
|
|
| 651 |
+ match self {
|
|
| 652 |
+ Self::InstallCoreAdi => "Install CoreADI", |
|
| 653 |
+ Self::LocateLibrary => "Locate Library", |
|
| 654 |
+ } |
|
| 655 |
+ .to_string() |
|
| 656 |
+ } |
|
| 657 |
+} |
|
| 658 |
+ |
|
| 59 | 659 |
#[derive(Clone, Debug)] |
| 60 | 660 |
pub(crate) struct AdiBackendOption {
|
| 61 |
- pub(crate) name: SharedString, |
|
| 62 |
- pub(crate) detail: SharedString, |
|
| 63 |
- pub(crate) status: SharedString, |
|
| 64 |
- pub(crate) information: SharedString, |
|
| 661 |
+ pub(crate) kind: AdiBackendKind, |
|
| 662 |
+ pub(crate) name: String, |
|
| 663 |
+ pub(crate) detail: String, |
|
| 664 |
+ pub(crate) availability: AdiBackendAvailability, |
|
| 665 |
+ pub(crate) details: Vec<AdiBackendDetail>, |
|
| 666 |
+ pub(crate) provisioning_state: AdiProvisioningState, |
|
| 65 | 667 |
pub(crate) editable_identity: bool, |
| 66 |
- pub(crate) repair_action: Option<SharedString>, |
|
| 668 |
+ pub(crate) repair_action: Option<AdiRepairAction>, |
|
| 67 | 669 |
} |
| 68 | 670 |
|
| 69 | 671 |
#[derive(Clone, Debug)] |
| ... | ... |
@@ -129,14 +731,39 @@ impl SideloadOperation {
|
| 129 | 731 |
} |
| 130 | 732 |
} |
| 131 | 733 |
|
| 132 |
- pub(crate) fn status_text(&self, app: &AppOption, device: &DeviceOption) -> SharedString {
|
|
| 734 |
+ pub(crate) fn status_text( |
|
| 735 |
+ &self, |
|
| 736 |
+ app: Option<&AppOption>, |
|
| 737 |
+ device: Option<&DeviceOption>, |
|
| 738 |
+ ) -> SharedString {
|
|
| 133 | 739 |
match self {
|
| 134 |
- Self::Idle => "Ready to install".into(), |
|
| 740 |
+ Self::Idle => {
|
|
| 741 |
+ if app.is_none() {
|
|
| 742 |
+ "Choose an IPA".into() |
|
| 743 |
+ } else if device.is_some() {
|
|
| 744 |
+ "Ready to install".into() |
|
| 745 |
+ } else {
|
|
| 746 |
+ "Connect a device".into() |
|
| 747 |
+ } |
|
| 748 |
+ } |
|
| 135 | 749 |
Self::Running { phase, .. } => match phase {
|
| 136 |
- SideloadPhase::Signing => format!("Signing {}", app.name).into(),
|
|
| 137 |
- SideloadPhase::Installing => format!("Installing to {}", device.name).into(),
|
|
| 750 |
+ SideloadPhase::Signing => {
|
|
| 751 |
+ let app_name = app.map(|app| app.name().as_ref()).unwrap_or("app");
|
|
| 752 |
+ format!("Signing {app_name}").into()
|
|
| 753 |
+ } |
|
| 754 |
+ SideloadPhase::Installing => {
|
|
| 755 |
+ let device_name = device |
|
| 756 |
+ .map(|device| device.name.as_ref()) |
|
| 757 |
+ .unwrap_or("device");
|
|
| 758 |
+ format!("Installing to {device_name}").into()
|
|
| 759 |
+ } |
|
| 138 | 760 |
}, |
| 139 |
- Self::Finished => format!("Installed on {}", device.name).into(),
|
|
| 761 |
+ Self::Finished => {
|
|
| 762 |
+ let device_name = device |
|
| 763 |
+ .map(|device| device.name.as_ref()) |
|
| 764 |
+ .unwrap_or("device");
|
|
| 765 |
+ format!("Installed on {device_name}").into()
|
|
| 766 |
+ } |
|
| 140 | 767 |
Self::Failed { message } => message.clone(),
|
| 141 | 768 |
} |
| 142 | 769 |
} |
| ... | ... |
@@ -0,0 +1,14 @@ |
| 1 |
+use directories::ProjectDirs; |
|
| 2 |
+use std::path::PathBuf; |
|
| 3 |
+ |
|
| 4 |
+const QUALIFIER: &str = "com"; |
|
| 5 |
+const ORGANIZATION: &str = "Dadoum"; |
|
| 6 |
+const APPLICATION: &str = "Super Sideloader"; |
|
| 7 |
+ |
|
| 8 |
+pub(crate) fn app_data_dir() -> Option<PathBuf> {
|
|
| 9 |
+ project_dirs().map(|dirs| dirs.data_dir().to_path_buf()) |
|
| 10 |
+} |
|
| 11 |
+ |
|
| 12 |
+fn project_dirs() -> Option<ProjectDirs> {
|
|
| 13 |
+ ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) |
|
| 14 |
+} |
| ... | ... |
@@ -0,0 +1,291 @@ |
| 1 |
+use crate::models::{
|
|
| 2 |
+ AdiBackendKind, AppEntitlement, AppOption, EntitlementValue, MachineIdentity, |
|
| 3 |
+ SupportedDeviceFamily, |
|
| 4 |
+}; |
|
| 5 |
+use crate::paths::app_data_dir; |
|
| 6 |
+use serde::{Deserialize, Serialize};
|
|
| 7 |
+use std::fs; |
|
| 8 |
+use std::io::ErrorKind; |
|
| 9 |
+use std::path::PathBuf; |
|
| 10 |
+ |
|
| 11 |
+const SETTINGS_FILE: &str = "settings.toml"; |
|
| 12 |
+ |
|
| 13 |
+#[derive(Clone, Debug, Default, Deserialize, Serialize)] |
|
| 14 |
+#[serde(default)] |
|
| 15 |
+pub(crate) struct SideloaderPreferences {
|
|
| 16 |
+ pub(crate) developer: DeveloperPreferences, |
|
| 17 |
+ pub(crate) app: AppPreferences, |
|
| 18 |
+ pub(crate) device: Option<DevicePreferences>, |
|
| 19 |
+ pub(crate) adi: AdiPreferences, |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+#[derive(Clone, Debug, Deserialize, Serialize)] |
|
| 23 |
+#[serde(default)] |
|
| 24 |
+pub(crate) struct DeveloperPreferences {
|
|
| 25 |
+ pub(crate) account_id: Option<String>, |
|
| 26 |
+ pub(crate) team_id: Option<String>, |
|
| 27 |
+ pub(crate) auto_app_id: bool, |
|
| 28 |
+ pub(crate) app_id: Option<String>, |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+impl Default for DeveloperPreferences {
|
|
| 32 |
+ fn default() -> Self {
|
|
| 33 |
+ Self {
|
|
| 34 |
+ account_id: None, |
|
| 35 |
+ team_id: None, |
|
| 36 |
+ auto_app_id: true, |
|
| 37 |
+ app_id: None, |
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+#[derive(Clone, Debug, Default, Deserialize, Serialize)] |
|
| 43 |
+#[serde(default)] |
|
| 44 |
+pub(crate) struct AppPreferences {
|
|
| 45 |
+ pub(crate) bundle_id: Option<String>, |
|
| 46 |
+ pub(crate) path: Option<String>, |
|
| 47 |
+ pub(crate) overrides: AppOverridePreferences, |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+#[derive(Clone, Debug, Default, Deserialize, Serialize)] |
|
| 51 |
+#[serde(default)] |
|
| 52 |
+pub(crate) struct AppOverridePreferences {
|
|
| 53 |
+ pub(crate) name: Option<String>, |
|
| 54 |
+ pub(crate) bundle_id: Option<String>, |
|
| 55 |
+ pub(crate) version: Option<String>, |
|
| 56 |
+ pub(crate) build: Option<String>, |
|
| 57 |
+ pub(crate) executable: Option<String>, |
|
| 58 |
+ pub(crate) minimum_os: Option<String>, |
|
| 59 |
+ pub(crate) supported_devices: Option<String>, |
|
| 60 |
+ pub(crate) icon_path: Option<String>, |
|
| 61 |
+ pub(crate) entitlements: Option<Vec<EntitlementPreferences>>, |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+#[derive(Clone, Debug, Deserialize, Serialize)] |
|
| 65 |
+pub(crate) struct EntitlementPreferences {
|
|
| 66 |
+ pub(crate) key: String, |
|
| 67 |
+ pub(crate) value_type: String, |
|
| 68 |
+ pub(crate) value: String, |
|
| 69 |
+ #[serde(default, skip_serializing_if = "Option::is_none")] |
|
| 70 |
+ pub(crate) values: Option<Vec<String>>, |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 73 |
+#[derive(Clone, Debug, Deserialize, Serialize)] |
|
| 74 |
+pub(crate) struct DevicePreferences {
|
|
| 75 |
+ pub(crate) udid: String, |
|
| 76 |
+ pub(crate) connection: String, |
|
| 77 |
+} |
|
| 78 |
+ |
|
| 79 |
+#[derive(Clone, Debug, Default, Deserialize, Serialize)] |
|
| 80 |
+#[serde(default)] |
|
| 81 |
+pub(crate) struct AdiPreferences {
|
|
| 82 |
+ pub(crate) backend: Option<StoredAdiBackendKind>, |
|
| 83 |
+ pub(crate) machine: MachineIdentityPreferences, |
|
| 84 |
+ pub(crate) android_adi_identifier: Option<String>, |
|
| 85 |
+ pub(crate) android_device: MachineIdentityPreferences, |
|
| 86 |
+ pub(crate) android_device_uuid: Option<String>, |
|
| 87 |
+ #[serde(skip_serializing)] |
|
| 88 |
+ pub(crate) android_machine: MachineIdentityPreferences, |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+#[derive(Clone, Debug, Default, Deserialize, Serialize)] |
|
| 92 |
+#[serde(default)] |
|
| 93 |
+pub(crate) struct MachineIdentityPreferences {
|
|
| 94 |
+ pub(crate) machine_name: Option<String>, |
|
| 95 |
+ pub(crate) os_name: Option<String>, |
|
| 96 |
+ pub(crate) os_version: Option<String>, |
|
| 97 |
+ pub(crate) machine_id: Option<String>, |
|
| 98 |
+} |
|
| 99 |
+ |
|
| 100 |
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] |
|
| 101 |
+#[serde(rename_all = "snake_case")] |
|
| 102 |
+pub(crate) enum StoredAdiBackendKind {
|
|
| 103 |
+ SystemAdid, |
|
| 104 |
+ WindowsCoreAdi, |
|
| 105 |
+ AndroidCoreAdi, |
|
| 106 |
+} |
|
| 107 |
+ |
|
| 108 |
+impl From<AdiBackendKind> for StoredAdiBackendKind {
|
|
| 109 |
+ fn from(kind: AdiBackendKind) -> Self {
|
|
| 110 |
+ match kind {
|
|
| 111 |
+ AdiBackendKind::SystemAdid => Self::SystemAdid, |
|
| 112 |
+ AdiBackendKind::WindowsCoreAdi => Self::WindowsCoreAdi, |
|
| 113 |
+ AdiBackendKind::AndroidCoreAdi => Self::AndroidCoreAdi, |
|
| 114 |
+ } |
|
| 115 |
+ } |
|
| 116 |
+} |
|
| 117 |
+ |
|
| 118 |
+impl From<StoredAdiBackendKind> for AdiBackendKind {
|
|
| 119 |
+ fn from(kind: StoredAdiBackendKind) -> Self {
|
|
| 120 |
+ match kind {
|
|
| 121 |
+ StoredAdiBackendKind::SystemAdid => Self::SystemAdid, |
|
| 122 |
+ StoredAdiBackendKind::WindowsCoreAdi => Self::WindowsCoreAdi, |
|
| 123 |
+ StoredAdiBackendKind::AndroidCoreAdi => Self::AndroidCoreAdi, |
|
| 124 |
+ } |
|
| 125 |
+ } |
|
| 126 |
+} |
|
| 127 |
+ |
|
| 128 |
+impl From<&MachineIdentity> for MachineIdentityPreferences {
|
|
| 129 |
+ fn from(identity: &MachineIdentity) -> Self {
|
|
| 130 |
+ Self {
|
|
| 131 |
+ machine_name: Some(identity.machine_name.to_string()), |
|
| 132 |
+ os_name: Some(identity.os_name.to_string()), |
|
| 133 |
+ os_version: Some(identity.os_version.to_string()), |
|
| 134 |
+ machine_id: Some(identity.machine_id.to_string()), |
|
| 135 |
+ } |
|
| 136 |
+ } |
|
| 137 |
+} |
|
| 138 |
+ |
|
| 139 |
+impl From<&AppEntitlement> for EntitlementPreferences {
|
|
| 140 |
+ fn from(entitlement: &AppEntitlement) -> Self {
|
|
| 141 |
+ Self {
|
|
| 142 |
+ key: entitlement.key.to_string(), |
|
| 143 |
+ value_type: entitlement.value.type_label().to_string(), |
|
| 144 |
+ value: entitlement.value.edit_text(), |
|
| 145 |
+ values: matches!(entitlement.value, EntitlementValue::Array(_)) |
|
| 146 |
+ .then(|| entitlement.value.array_edit_values()), |
|
| 147 |
+ } |
|
| 148 |
+ } |
|
| 149 |
+} |
|
| 150 |
+ |
|
| 151 |
+impl From<&EntitlementPreferences> for AppEntitlement {
|
|
| 152 |
+ fn from(entitlement: &EntitlementPreferences) -> Self {
|
|
| 153 |
+ let value = if entitlement.value_type == "Array" {
|
|
| 154 |
+ entitlement |
|
| 155 |
+ .values |
|
| 156 |
+ .clone() |
|
| 157 |
+ .map(EntitlementValue::string_array) |
|
| 158 |
+ .unwrap_or_else(|| {
|
|
| 159 |
+ EntitlementValue::from_type_and_text( |
|
| 160 |
+ &entitlement.value_type, |
|
| 161 |
+ &entitlement.value, |
|
| 162 |
+ ) |
|
| 163 |
+ }) |
|
| 164 |
+ } else {
|
|
| 165 |
+ EntitlementValue::from_type_and_text(&entitlement.value_type, &entitlement.value) |
|
| 166 |
+ }; |
|
| 167 |
+ |
|
| 168 |
+ Self {
|
|
| 169 |
+ key: entitlement.key.clone().into(), |
|
| 170 |
+ value, |
|
| 171 |
+ } |
|
| 172 |
+ } |
|
| 173 |
+} |
|
| 174 |
+ |
|
| 175 |
+impl From<&AppOption> for AppOverridePreferences {
|
|
| 176 |
+ fn from(app: &AppOption) -> Self {
|
|
| 177 |
+ Self {
|
|
| 178 |
+ name: app |
|
| 179 |
+ .metadata |
|
| 180 |
+ .name |
|
| 181 |
+ .override_value |
|
| 182 |
+ .as_ref() |
|
| 183 |
+ .map(ToString::to_string), |
|
| 184 |
+ bundle_id: app |
|
| 185 |
+ .metadata |
|
| 186 |
+ .bundle_id |
|
| 187 |
+ .override_value |
|
| 188 |
+ .as_ref() |
|
| 189 |
+ .map(ToString::to_string), |
|
| 190 |
+ version: app |
|
| 191 |
+ .metadata |
|
| 192 |
+ .version |
|
| 193 |
+ .override_value |
|
| 194 |
+ .as_ref() |
|
| 195 |
+ .map(ToString::to_string), |
|
| 196 |
+ build: app |
|
| 197 |
+ .metadata |
|
| 198 |
+ .build |
|
| 199 |
+ .override_value |
|
| 200 |
+ .as_ref() |
|
| 201 |
+ .map(ToString::to_string), |
|
| 202 |
+ executable: app |
|
| 203 |
+ .metadata |
|
| 204 |
+ .executable |
|
| 205 |
+ .override_value |
|
| 206 |
+ .as_ref() |
|
| 207 |
+ .map(ToString::to_string), |
|
| 208 |
+ minimum_os: app |
|
| 209 |
+ .metadata |
|
| 210 |
+ .minimum_os |
|
| 211 |
+ .override_value |
|
| 212 |
+ .as_ref() |
|
| 213 |
+ .map(ToString::to_string), |
|
| 214 |
+ supported_devices: app |
|
| 215 |
+ .metadata |
|
| 216 |
+ .supported_devices |
|
| 217 |
+ .override_value |
|
| 218 |
+ .as_ref() |
|
| 219 |
+ .map(|devices| SupportedDeviceFamily::display_list(devices).to_string()), |
|
| 220 |
+ icon_path: app.icon_override_path.as_ref().map(ToString::to_string), |
|
| 221 |
+ entitlements: app.entitlement_overrides.as_ref().map(|entitlements| {
|
|
| 222 |
+ entitlements |
|
| 223 |
+ .iter() |
|
| 224 |
+ .map(EntitlementPreferences::from) |
|
| 225 |
+ .collect() |
|
| 226 |
+ }), |
|
| 227 |
+ } |
|
| 228 |
+ } |
|
| 229 |
+} |
|
| 230 |
+ |
|
| 231 |
+pub(crate) fn apply_app_overrides(app: &mut AppOption, overrides: &AppOverridePreferences) {
|
|
| 232 |
+ app.metadata.name.override_value = overrides.name.clone().map(Into::into); |
|
| 233 |
+ app.metadata.bundle_id.override_value = overrides.bundle_id.clone().map(Into::into); |
|
| 234 |
+ app.metadata.version.override_value = overrides.version.clone().map(Into::into); |
|
| 235 |
+ app.metadata.build.override_value = overrides.build.clone().map(Into::into); |
|
| 236 |
+ app.metadata.executable.override_value = overrides.executable.clone().map(Into::into); |
|
| 237 |
+ app.metadata.minimum_os.override_value = overrides.minimum_os.clone().map(Into::into); |
|
| 238 |
+ app.metadata.supported_devices.override_value = overrides |
|
| 239 |
+ .supported_devices |
|
| 240 |
+ .as_ref() |
|
| 241 |
+ .map(|devices| SupportedDeviceFamily::parse_list(devices)); |
|
| 242 |
+ app.icon_override_path = overrides.icon_path.clone().map(Into::into); |
|
| 243 |
+ app.entitlement_overrides = overrides.entitlements.as_ref().map(|entitlements| {
|
|
| 244 |
+ entitlements |
|
| 245 |
+ .iter() |
|
| 246 |
+ .map(AppEntitlement::from) |
|
| 247 |
+ .collect::<Vec<_>>() |
|
| 248 |
+ }); |
|
| 249 |
+} |
|
| 250 |
+ |
|
| 251 |
+pub(crate) fn load_preferences() -> Result<SideloaderPreferences, String> {
|
|
| 252 |
+ let path = settings_path()?; |
|
| 253 |
+ let contents = match fs::read_to_string(&path) {
|
|
| 254 |
+ Ok(contents) => contents, |
|
| 255 |
+ Err(error) if error.kind() == ErrorKind::NotFound => {
|
|
| 256 |
+ return Ok(SideloaderPreferences::default()); |
|
| 257 |
+ } |
|
| 258 |
+ Err(error) => {
|
|
| 259 |
+ return Err(format!( |
|
| 260 |
+ "Failed to read settings at {}: {error}",
|
|
| 261 |
+ path.display() |
|
| 262 |
+ )); |
|
| 263 |
+ } |
|
| 264 |
+ }; |
|
| 265 |
+ |
|
| 266 |
+ toml::from_str(&contents) |
|
| 267 |
+ .map_err(|error| format!("Failed to parse settings at {}: {error}", path.display()))
|
|
| 268 |
+} |
|
| 269 |
+ |
|
| 270 |
+pub(crate) fn save_preferences(preferences: &SideloaderPreferences) -> Result<(), String> {
|
|
| 271 |
+ let path = settings_path()?; |
|
| 272 |
+ if let Some(parent) = path.parent() {
|
|
| 273 |
+ fs::create_dir_all(parent).map_err(|error| {
|
|
| 274 |
+ format!( |
|
| 275 |
+ "Failed to create settings folder at {}: {error}",
|
|
| 276 |
+ parent.display() |
|
| 277 |
+ ) |
|
| 278 |
+ })?; |
|
| 279 |
+ } |
|
| 280 |
+ |
|
| 281 |
+ let contents = toml::to_string_pretty(preferences) |
|
| 282 |
+ .map_err(|error| format!("Failed to encode settings: {error}"))?;
|
|
| 283 |
+ fs::write(&path, contents) |
|
| 284 |
+ .map_err(|error| format!("Failed to write settings at {}: {error}", path.display()))
|
|
| 285 |
+} |
|
| 286 |
+ |
|
| 287 |
+fn settings_path() -> Result<PathBuf, String> {
|
|
| 288 |
+ app_data_dir() |
|
| 289 |
+ .map(|path| path.join(SETTINGS_FILE)) |
|
| 290 |
+ .ok_or_else(|| "The application data folder is not available.".to_string()) |
|
| 291 |
+} |
| ... | ... |
@@ -1,19 +1,40 @@ |
| 1 |
+mod adi_view; |
|
| 2 |
+mod app_view; |
|
| 3 |
+mod developer_login_view; |
|
| 4 |
+mod developer_view; |
|
| 5 |
+use crate::adi_services::{self, CoreAdiInstallEvent};
|
|
| 1 | 6 |
use crate::constants::*; |
| 7 |
+use crate::developer_accounts::{
|
|
| 8 |
+ mock_developer_login, mock_developer_login_with_code, MockDeveloperLoginOutcome, |
|
| 9 |
+}; |
|
| 10 |
+use crate::edit_line::EditLine; |
|
| 2 | 11 |
use crate::main_view::SideloaderView; |
| 3 |
-use crate::models::{AdiBackendOption, AppIdOption, AppOption, MachineIdentity, TeamOption};
|
|
| 12 |
+use crate::models::{
|
|
| 13 |
+ AccountOption, AdiBackendKind, AdiBackendOption, AdiProvisioningState, AppEntitlement, |
|
| 14 |
+ AppIdOption, AppMetadataField, AppOption, EntitlementValue, MachineIdentity, |
|
| 15 |
+ SupportedDeviceFamily, TeamOption, |
|
| 16 |
+}; |
|
| 17 |
+use crate::paths::app_data_dir; |
|
| 4 | 18 |
use crate::widgets::{
|
| 5 | 19 |
chevron, combo_button, combo_item_content, combo_with_popover, developer_account_title, |
| 6 |
- dropdown_list, lucide_icon, properties_list, |
|
| 20 |
+ dropdown_list, lucide_icon, progress_circle, properties_list, |
|
| 7 | 21 |
}; |
| 22 |
+use futures::{channel::mpsc, StreamExt};
|
|
| 8 | 23 |
use gpui::{
|
| 9 |
- anchored, deferred, div, point, prelude::*, px, rgb, size, AnchoredPositionMode, App, |
|
| 10 |
- AppContext, Bounds, ClickEvent, Context, FocusHandle, FontWeight, InteractiveElement, |
|
| 11 |
- IntoElement, ParentElement, Render, ScrollHandle, SharedString, StatefulInteractiveElement, |
|
| 12 |
- Styled, WeakEntity, Window, WindowBounds, WindowHandle, WindowKind, WindowOptions, |
|
| 24 |
+ anchored, deferred, div, img, point, prelude::*, px, rgb, size, AnchoredPositionMode, App, |
|
| 25 |
+ AppContext, Bounds, ClickEvent, Context, Entity, FocusHandle, FontWeight, InteractiveElement, |
|
| 26 |
+ IntoElement, KeyDownEvent, ObjectFit, ParentElement, PathPromptOptions, Render, ScrollHandle, |
|
| 27 |
+ SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window, WindowBounds, |
|
| 28 |
+ WindowHandle, WindowKind, WindowOptions, |
|
| 13 | 29 |
}; |
| 30 |
+use std::sync::Arc; |
|
| 31 |
+use std::{fs, process::Command};
|
|
| 14 | 32 |
|
| 15 | 33 |
#[derive(Clone)] |
| 16 | 34 |
pub(crate) enum SettingsWindowRequest {
|
| 35 |
+ DeveloperLogin {
|
|
| 36 |
+ parent: WeakEntity<SideloaderView>, |
|
| 37 |
+ }, |
|
| 17 | 38 |
Team {
|
| 18 | 39 |
parent: WeakEntity<SideloaderView>, |
| 19 | 40 |
teams: Vec<TeamOption>, |
| ... | ... |
@@ -22,15 +43,19 @@ pub(crate) enum SettingsWindowRequest {
|
| 22 | 43 |
selected_app_id: usize, |
| 23 | 44 |
}, |
| 24 | 45 |
AppSettings {
|
| 25 |
- _parent: WeakEntity<SideloaderView>, |
|
| 46 |
+ parent: WeakEntity<SideloaderView>, |
|
| 47 |
+ app_index: usize, |
|
| 26 | 48 |
app: AppOption, |
| 27 | 49 |
enabled_patches: Vec<bool>, |
| 50 |
+ team_id: SharedString, |
|
| 28 | 51 |
}, |
| 29 | 52 |
AdiSettings {
|
| 30 | 53 |
parent: WeakEntity<SideloaderView>, |
| 31 | 54 |
backends: Vec<AdiBackendOption>, |
| 32 | 55 |
selected_backend: usize, |
| 33 | 56 |
machine_identity: MachineIdentity, |
| 57 |
+ android_device_identity: MachineIdentity, |
|
| 58 |
+ android_adi_identifier: String, |
|
| 34 | 59 |
}, |
| 35 | 60 |
} |
| 36 | 61 |
|
| ... | ... |
@@ -41,10 +66,222 @@ pub(crate) struct SettingsWindow {
|
| 41 | 66 |
team_picker_open: bool, |
| 42 | 67 |
app_id_picker_open: bool, |
| 43 | 68 |
adi_backend_picker_open: bool, |
| 69 |
+ adi_operation: Option<AdiOperationState>, |
|
| 70 |
+ machine_identity_edit: Option<MachineIdentityEdit>, |
|
| 71 |
+ app_detail_edit: Option<AppDetailEdit>, |
|
| 72 |
+ selected_entitlement: Option<usize>, |
|
| 73 |
+ entitlement_edit: Option<EntitlementEdit>, |
|
| 74 |
+ entitlement_type_picker_open: bool, |
|
| 75 |
+ developer_login: DeveloperLoginState, |
|
| 76 |
+ spinner_turns: f32, |
|
| 77 |
+} |
|
| 78 |
+ |
|
| 79 |
+#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
|
| 80 |
+enum DeveloperLoginStep {
|
|
| 81 |
+ Credentials, |
|
| 82 |
+ TwoFactor, |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+struct DeveloperLoginState {
|
|
| 86 |
+ email: Entity<EditLine>, |
|
| 87 |
+ password: Entity<EditLine>, |
|
| 88 |
+ code: Entity<EditLine>, |
|
| 89 |
+ remember_account: bool, |
|
| 90 |
+ step: DeveloperLoginStep, |
|
| 91 |
+ two_factor_detail: SharedString, |
|
| 92 |
+ error: Option<SharedString>, |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+impl DeveloperLoginState {
|
|
| 96 |
+ fn new(cx: &mut Context<SettingsWindow>) -> Self {
|
|
| 97 |
+ Self {
|
|
| 98 |
+ email: cx.new(|cx| EditLine::new("", cx)),
|
|
| 99 |
+ password: cx.new(|cx| EditLine::new_password("", cx)),
|
|
| 100 |
+ code: cx.new(|cx| EditLine::new("", cx)),
|
|
| 101 |
+ remember_account: false, |
|
| 102 |
+ step: DeveloperLoginStep::Credentials, |
|
| 103 |
+ two_factor_detail: "".into(), |
|
| 104 |
+ error: None, |
|
| 105 |
+ } |
|
| 106 |
+ } |
|
| 107 |
+} |
|
| 108 |
+ |
|
| 109 |
+#[derive(Clone, Debug)] |
|
| 110 |
+enum AdiOperationState {
|
|
| 111 |
+ DownloadingCoreAdi {
|
|
| 112 |
+ downloaded_bytes: u64, |
|
| 113 |
+ total_bytes: Option<u64>, |
|
| 114 |
+ }, |
|
| 115 |
+ InstallingCoreAdi, |
|
| 116 |
+ Provisioning, |
|
| 117 |
+} |
|
| 118 |
+ |
|
| 119 |
+#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
|
| 120 |
+enum MachineIdentityField {
|
|
| 121 |
+ MachineModel, |
|
| 122 |
+ OsName, |
|
| 123 |
+ OsVersion, |
|
| 124 |
+ MachineId, |
|
| 125 |
+} |
|
| 126 |
+ |
|
| 127 |
+impl MachineIdentityField {
|
|
| 128 |
+ fn label(self) -> &'static str {
|
|
| 129 |
+ match self {
|
|
| 130 |
+ Self::MachineModel => "Machine model", |
|
| 131 |
+ Self::OsName => "OS name", |
|
| 132 |
+ Self::OsVersion => "OS version", |
|
| 133 |
+ Self::MachineId => "Machine ID", |
|
| 134 |
+ } |
|
| 135 |
+ } |
|
| 136 |
+ |
|
| 137 |
+ fn value(self, identity: &MachineIdentity) -> &SharedString {
|
|
| 138 |
+ match self {
|
|
| 139 |
+ Self::MachineModel => &identity.machine_name, |
|
| 140 |
+ Self::OsName => &identity.os_name, |
|
| 141 |
+ Self::OsVersion => &identity.os_version, |
|
| 142 |
+ Self::MachineId => &identity.machine_id, |
|
| 143 |
+ } |
|
| 144 |
+ } |
|
| 145 |
+ |
|
| 146 |
+ fn set_value(self, identity: &mut MachineIdentity, value: String) {
|
|
| 147 |
+ match self {
|
|
| 148 |
+ Self::MachineModel => identity.machine_name = value.into(), |
|
| 149 |
+ Self::OsName => identity.os_name = value.into(), |
|
| 150 |
+ Self::OsVersion => identity.os_version = value.into(), |
|
| 151 |
+ Self::MachineId => identity.machine_id = value.into(), |
|
| 152 |
+ } |
|
| 153 |
+ } |
|
| 154 |
+} |
|
| 155 |
+ |
|
| 156 |
+#[derive(Clone)] |
|
| 157 |
+struct MachineIdentityEdit {
|
|
| 158 |
+ field: MachineIdentityField, |
|
| 159 |
+ input: Entity<EditLine>, |
|
| 160 |
+} |
|
| 161 |
+ |
|
| 162 |
+#[derive(Clone)] |
|
| 163 |
+enum AppDetailEdit {
|
|
| 164 |
+ Text {
|
|
| 165 |
+ field: AppMetadataField, |
|
| 166 |
+ input: Entity<EditLine>, |
|
| 167 |
+ }, |
|
| 168 |
+ SupportedDevices {
|
|
| 169 |
+ selected: Vec<SupportedDeviceFamily>, |
|
| 170 |
+ }, |
|
| 171 |
+} |
|
| 172 |
+ |
|
| 173 |
+impl AppDetailEdit {
|
|
| 174 |
+ fn field(&self) -> AppMetadataField {
|
|
| 175 |
+ match self {
|
|
| 176 |
+ Self::Text { field, .. } => *field,
|
|
| 177 |
+ Self::SupportedDevices { .. } => AppMetadataField::SupportedDevices,
|
|
| 178 |
+ } |
|
| 179 |
+ } |
|
| 180 |
+} |
|
| 181 |
+ |
|
| 182 |
+#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
|
| 183 |
+enum EntitlementField {
|
|
| 184 |
+ Key, |
|
| 185 |
+ ValueType, |
|
| 186 |
+ Value, |
|
| 187 |
+} |
|
| 188 |
+ |
|
| 189 |
+#[derive(Clone)] |
|
| 190 |
+enum EntitlementEdit {
|
|
| 191 |
+ Text {
|
|
| 192 |
+ row: usize, |
|
| 193 |
+ field: EntitlementField, |
|
| 194 |
+ input: Entity<EditLine>, |
|
| 195 |
+ }, |
|
| 196 |
+ Boolean {
|
|
| 197 |
+ row: usize, |
|
| 198 |
+ value: bool, |
|
| 199 |
+ }, |
|
| 200 |
+ Array {
|
|
| 201 |
+ row: usize, |
|
| 202 |
+ items: Vec<Entity<EditLine>>, |
|
| 203 |
+ }, |
|
| 204 |
+ Type {
|
|
| 205 |
+ row: usize, |
|
| 206 |
+ selected: SharedString, |
|
| 207 |
+ }, |
|
| 208 |
+} |
|
| 209 |
+ |
|
| 210 |
+impl EntitlementEdit {
|
|
| 211 |
+ fn row(&self) -> usize {
|
|
| 212 |
+ match self {
|
|
| 213 |
+ Self::Text { row, .. }
|
|
| 214 |
+ | Self::Boolean { row, .. }
|
|
| 215 |
+ | Self::Array { row, .. }
|
|
| 216 |
+ | Self::Type { row, .. } => *row,
|
|
| 217 |
+ } |
|
| 218 |
+ } |
|
| 219 |
+ |
|
| 220 |
+ fn field(&self) -> EntitlementField {
|
|
| 221 |
+ match self {
|
|
| 222 |
+ Self::Text { field, .. } => *field,
|
|
| 223 |
+ Self::Boolean { .. } | Self::Array { .. } => EntitlementField::Value,
|
|
| 224 |
+ Self::Type { .. } => EntitlementField::ValueType,
|
|
| 225 |
+ } |
|
| 226 |
+ } |
|
| 227 |
+} |
|
| 228 |
+ |
|
| 229 |
+impl AdiOperationState {
|
|
| 230 |
+ fn label(&self) -> SharedString {
|
|
| 231 |
+ match self {
|
|
| 232 |
+ Self::DownloadingCoreAdi {
|
|
| 233 |
+ downloaded_bytes, |
|
| 234 |
+ total_bytes: Some(total_bytes), |
|
| 235 |
+ } if *total_bytes > 0 => {
|
|
| 236 |
+ let percent = ((*downloaded_bytes as f32 / *total_bytes as f32) * 100.) |
|
| 237 |
+ .clamp(0., 99.) |
|
| 238 |
+ .round() as u32; |
|
| 239 |
+ format!("Downloading {percent}%").into()
|
|
| 240 |
+ } |
|
| 241 |
+ Self::DownloadingCoreAdi { .. } => "Downloading...".into(),
|
|
| 242 |
+ Self::InstallingCoreAdi => "Installing...".into(), |
|
| 243 |
+ Self::Provisioning => "Provisioning...".into(), |
|
| 244 |
+ } |
|
| 245 |
+ } |
|
| 246 |
+ |
|
| 247 |
+ fn progress(&self) -> f32 {
|
|
| 248 |
+ match self {
|
|
| 249 |
+ Self::DownloadingCoreAdi {
|
|
| 250 |
+ downloaded_bytes, |
|
| 251 |
+ total_bytes: Some(total_bytes), |
|
| 252 |
+ } if *total_bytes > 0 => (*downloaded_bytes as f32 / *total_bytes as f32).clamp(0., 1.), |
|
| 253 |
+ Self::DownloadingCoreAdi { .. } => 0.28,
|
|
| 254 |
+ Self::InstallingCoreAdi => 0.72, |
|
| 255 |
+ Self::Provisioning => 0.45, |
|
| 256 |
+ } |
|
| 257 |
+ } |
|
| 258 |
+ |
|
| 259 |
+ fn is_indeterminate(&self) -> bool {
|
|
| 260 |
+ matches!( |
|
| 261 |
+ self, |
|
| 262 |
+ Self::DownloadingCoreAdi {
|
|
| 263 |
+ total_bytes: None, |
|
| 264 |
+ .. |
|
| 265 |
+ } | Self::InstallingCoreAdi |
|
| 266 |
+ | Self::Provisioning |
|
| 267 |
+ ) |
|
| 268 |
+ } |
|
| 269 |
+ |
|
| 270 |
+ fn is_coreadi_install(&self) -> bool {
|
|
| 271 |
+ matches!( |
|
| 272 |
+ self, |
|
| 273 |
+ Self::DownloadingCoreAdi { .. } | Self::InstallingCoreAdi
|
|
| 274 |
+ ) |
|
| 275 |
+ } |
|
| 276 |
+ |
|
| 277 |
+ fn is_provisioning(&self) -> bool {
|
|
| 278 |
+ matches!(self, Self::Provisioning) |
|
| 279 |
+ } |
|
| 44 | 280 |
} |
| 45 | 281 |
impl SettingsWindowRequest {
|
| 46 | 282 |
fn title(&self) -> &'static str {
|
| 47 | 283 |
match self {
|
| 284 |
+ SettingsWindowRequest::DeveloperLogin { .. } => "Add Apple Account",
|
|
| 48 | 285 |
SettingsWindowRequest::Team { .. } => "Developer Settings",
|
| 49 | 286 |
SettingsWindowRequest::AppSettings { .. } => "App Settings",
|
| 50 | 287 |
SettingsWindowRequest::AdiSettings { .. } => "Settings",
|
| ... | ... |
@@ -65,6 +302,14 @@ impl SettingsWindow {
|
| 65 | 302 |
team_picker_open: false, |
| 66 | 303 |
app_id_picker_open: false, |
| 67 | 304 |
adi_backend_picker_open: false, |
| 305 |
+ adi_operation: None, |
|
| 306 |
+ machine_identity_edit: None, |
|
| 307 |
+ app_detail_edit: None, |
|
| 308 |
+ selected_entitlement: None, |
|
| 309 |
+ entitlement_edit: None, |
|
| 310 |
+ entitlement_type_picker_open: false, |
|
| 311 |
+ developer_login: DeveloperLoginState::new(cx), |
|
| 312 |
+ spinner_turns: 0., |
|
| 68 | 313 |
} |
| 69 | 314 |
} |
| 70 | 315 |
|
| ... | ... |
@@ -74,16 +319,189 @@ impl SettingsWindow {
|
| 74 | 319 |
window: &mut Window, |
| 75 | 320 |
cx: &mut Context<Self>, |
| 76 | 321 |
) {
|
| 322 |
+ let reset_login = matches!(&request, SettingsWindowRequest::DeveloperLogin { .. });
|
|
| 323 |
+ let title = request.title(); |
|
| 77 | 324 |
self.request = request; |
| 78 | 325 |
self.team_picker_open = false; |
| 79 | 326 |
self.app_id_picker_open = false; |
| 80 | 327 |
self.adi_backend_picker_open = false; |
| 328 |
+ self.machine_identity_edit = None; |
|
| 329 |
+ self.app_detail_edit = None; |
|
| 330 |
+ self.selected_entitlement = None; |
|
| 331 |
+ self.entitlement_edit = None; |
|
| 332 |
+ self.entitlement_type_picker_open = false; |
|
| 333 |
+ if reset_login {
|
|
| 334 |
+ self.developer_login = DeveloperLoginState::new(cx); |
|
| 335 |
+ } |
|
| 336 |
+ window.set_window_title(title); |
|
| 81 | 337 |
window.activate_window(); |
| 82 | 338 |
window.focus(&self.focus_handle, cx); |
| 83 | 339 |
cx.defer_in(window, |_, _, cx| cx.notify()); |
| 84 | 340 |
cx.notify(); |
| 85 | 341 |
} |
| 86 | 342 |
|
| 343 |
+ pub(crate) fn set_app_settings_team_id( |
|
| 344 |
+ &mut self, |
|
| 345 |
+ updated_team_id: SharedString, |
|
| 346 |
+ cx: &mut Context<Self>, |
|
| 347 |
+ ) {
|
|
| 348 |
+ let SettingsWindowRequest::AppSettings { team_id, .. } = &mut self.request else {
|
|
| 349 |
+ return; |
|
| 350 |
+ }; |
|
| 351 |
+ if team_id.as_ref() != updated_team_id.as_ref() {
|
|
| 352 |
+ *team_id = updated_team_id; |
|
| 353 |
+ cx.notify(); |
|
| 354 |
+ } |
|
| 355 |
+ } |
|
| 356 |
+ |
|
| 357 |
+ fn toggle_remember_developer_account( |
|
| 358 |
+ &mut self, |
|
| 359 |
+ _: &ClickEvent, |
|
| 360 |
+ window: &mut Window, |
|
| 361 |
+ cx: &mut Context<Self>, |
|
| 362 |
+ ) {
|
|
| 363 |
+ window.focus(&self.focus_handle, cx); |
|
| 364 |
+ if matches!(self.request, SettingsWindowRequest::DeveloperLogin { .. }) {
|
|
| 365 |
+ self.developer_login.remember_account = !self.developer_login.remember_account; |
|
| 366 |
+ cx.notify(); |
|
| 367 |
+ } |
|
| 368 |
+ } |
|
| 369 |
+ |
|
| 370 |
+ fn submit_developer_login( |
|
| 371 |
+ &mut self, |
|
| 372 |
+ _: &ClickEvent, |
|
| 373 |
+ window: &mut Window, |
|
| 374 |
+ cx: &mut Context<Self>, |
|
| 375 |
+ ) {
|
|
| 376 |
+ window.focus(&self.focus_handle, cx); |
|
| 377 |
+ self.submit_developer_login_inner(window, cx); |
|
| 378 |
+ } |
|
| 379 |
+ |
|
| 380 |
+ fn submit_developer_login_inner(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 381 |
+ let SettingsWindowRequest::DeveloperLogin { parent } = &self.request else {
|
|
| 382 |
+ return; |
|
| 383 |
+ }; |
|
| 384 |
+ let parent = parent.clone(); |
|
| 385 |
+ let email = self.developer_login.email.read(cx).value(); |
|
| 386 |
+ let password = self.developer_login.password.read(cx).value(); |
|
| 387 |
+ |
|
| 388 |
+ match mock_developer_login(&email, &password) {
|
|
| 389 |
+ Ok(MockDeveloperLoginOutcome::RequiresTwoFactor { detail }) => {
|
|
| 390 |
+ self.developer_login.step = DeveloperLoginStep::TwoFactor; |
|
| 391 |
+ self.developer_login.two_factor_detail = detail; |
|
| 392 |
+ self.developer_login.error = None; |
|
| 393 |
+ self.developer_login.code.update(cx, |code, cx| {
|
|
| 394 |
+ *code = EditLine::new("", cx);
|
|
| 395 |
+ }); |
|
| 396 |
+ let code = self.developer_login.code.clone(); |
|
| 397 |
+ code.update(cx, |code, cx| code.focus(window, cx)); |
|
| 398 |
+ cx.notify(); |
|
| 399 |
+ } |
|
| 400 |
+ Ok(MockDeveloperLoginOutcome::SignedIn(account)) => {
|
|
| 401 |
+ self.finish_developer_login(account, parent, window, cx); |
|
| 402 |
+ } |
|
| 403 |
+ Err(error) => {
|
|
| 404 |
+ self.developer_login.error = Some(error.into()); |
|
| 405 |
+ cx.notify(); |
|
| 406 |
+ } |
|
| 407 |
+ } |
|
| 408 |
+ } |
|
| 409 |
+ |
|
| 410 |
+ fn submit_developer_two_factor( |
|
| 411 |
+ &mut self, |
|
| 412 |
+ _: &ClickEvent, |
|
| 413 |
+ window: &mut Window, |
|
| 414 |
+ cx: &mut Context<Self>, |
|
| 415 |
+ ) {
|
|
| 416 |
+ window.focus(&self.focus_handle, cx); |
|
| 417 |
+ self.submit_developer_two_factor_inner(window, cx); |
|
| 418 |
+ } |
|
| 419 |
+ |
|
| 420 |
+ fn submit_developer_two_factor_inner(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 421 |
+ let SettingsWindowRequest::DeveloperLogin { parent } = &self.request else {
|
|
| 422 |
+ return; |
|
| 423 |
+ }; |
|
| 424 |
+ let parent = parent.clone(); |
|
| 425 |
+ let email = self.developer_login.email.read(cx).value(); |
|
| 426 |
+ let code = self.developer_login.code.read(cx).value(); |
|
| 427 |
+ |
|
| 428 |
+ match mock_developer_login_with_code(&email, &code) {
|
|
| 429 |
+ Ok(account) => self.finish_developer_login(account, parent, window, cx), |
|
| 430 |
+ Err(error) => {
|
|
| 431 |
+ self.developer_login.error = Some(error.into()); |
|
| 432 |
+ cx.notify(); |
|
| 433 |
+ } |
|
| 434 |
+ } |
|
| 435 |
+ } |
|
| 436 |
+ |
|
| 437 |
+ fn back_to_developer_login( |
|
| 438 |
+ &mut self, |
|
| 439 |
+ _: &ClickEvent, |
|
| 440 |
+ window: &mut Window, |
|
| 441 |
+ cx: &mut Context<Self>, |
|
| 442 |
+ ) {
|
|
| 443 |
+ window.focus(&self.focus_handle, cx); |
|
| 444 |
+ if matches!(self.request, SettingsWindowRequest::DeveloperLogin { .. }) {
|
|
| 445 |
+ self.developer_login.step = DeveloperLoginStep::Credentials; |
|
| 446 |
+ self.developer_login.error = None; |
|
| 447 |
+ cx.notify(); |
|
| 448 |
+ } |
|
| 449 |
+ } |
|
| 450 |
+ |
|
| 451 |
+ fn handle_developer_login_key( |
|
| 452 |
+ &mut self, |
|
| 453 |
+ event: &KeyDownEvent, |
|
| 454 |
+ window: &mut Window, |
|
| 455 |
+ cx: &mut Context<Self>, |
|
| 456 |
+ ) {
|
|
| 457 |
+ if !matches!(self.request, SettingsWindowRequest::DeveloperLogin { .. }) {
|
|
| 458 |
+ return; |
|
| 459 |
+ } |
|
| 460 |
+ |
|
| 461 |
+ if event.keystroke.key.as_str() == "enter" {
|
|
| 462 |
+ cx.stop_propagation(); |
|
| 463 |
+ window.prevent_default(); |
|
| 464 |
+ match self.developer_login.step {
|
|
| 465 |
+ DeveloperLoginStep::Credentials => {
|
|
| 466 |
+ self.submit_developer_login_inner(window, cx); |
|
| 467 |
+ } |
|
| 468 |
+ DeveloperLoginStep::TwoFactor => {
|
|
| 469 |
+ self.submit_developer_two_factor_inner(window, cx); |
|
| 470 |
+ } |
|
| 471 |
+ } |
|
| 472 |
+ } |
|
| 473 |
+ } |
|
| 474 |
+ |
|
| 475 |
+ fn finish_developer_login( |
|
| 476 |
+ &mut self, |
|
| 477 |
+ mut account: AccountOption, |
|
| 478 |
+ parent: WeakEntity<SideloaderView>, |
|
| 479 |
+ window: &mut Window, |
|
| 480 |
+ cx: &mut Context<Self>, |
|
| 481 |
+ ) {
|
|
| 482 |
+ account.detail = if self.developer_login.remember_account {
|
|
| 483 |
+ "Mock login session, marked to remember".into() |
|
| 484 |
+ } else {
|
|
| 485 |
+ "Mock login session".into() |
|
| 486 |
+ }; |
|
| 487 |
+ |
|
| 488 |
+ match parent.update(cx, |view, cx| {
|
|
| 489 |
+ view.add_developer_account_from_settings(account, cx) |
|
| 490 |
+ }) {
|
|
| 491 |
+ Ok(Some(request)) => self.show_request(request, window, cx), |
|
| 492 |
+ Ok(None) => {
|
|
| 493 |
+ self.developer_login.error = |
|
| 494 |
+ Some("The mock account has no developer teams.".into());
|
|
| 495 |
+ cx.notify(); |
|
| 496 |
+ } |
|
| 497 |
+ Err(_) => {
|
|
| 498 |
+ self.developer_login.error = |
|
| 499 |
+ Some("The main sideloader window is not available.".into());
|
|
| 500 |
+ cx.notify(); |
|
| 501 |
+ } |
|
| 502 |
+ } |
|
| 503 |
+ } |
|
| 504 |
+ |
|
| 87 | 505 |
fn select_team( |
| 88 | 506 |
&mut self, |
| 89 | 507 |
index: usize, |
| ... | ... |
@@ -111,10 +529,12 @@ impl SettingsWindow {
|
| 111 | 529 |
self.team_picker_open = false; |
| 112 | 530 |
self.app_id_picker_open = false; |
| 113 | 531 |
let _ = parent.update(cx, |view, cx| {
|
| 114 |
- let team_count = view.accounts[view.selected_account].teams.len(); |
|
| 115 |
- if index < team_count {
|
|
| 116 |
- view.selected_team = index; |
|
| 117 |
- cx.notify(); |
|
| 532 |
+ if let Some(team_id) = view.select_team_from_settings(index, cx) {
|
|
| 533 |
+ if let Some(handle) = view.app_settings_window.clone() {
|
|
| 534 |
+ let _ = handle.update(cx, |settings, _, cx| {
|
|
| 535 |
+ settings.set_app_settings_team_id(team_id.clone(), cx); |
|
| 536 |
+ }); |
|
| 537 |
+ } |
|
| 118 | 538 |
} |
| 119 | 539 |
}); |
| 120 | 540 |
cx.notify(); |
| ... | ... |
@@ -131,12 +551,21 @@ impl SettingsWindow {
|
| 131 | 551 |
|
| 132 | 552 |
fn toggle_auto_app_id(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
| 133 | 553 |
window.focus(&self.focus_handle, cx); |
| 134 |
- let SettingsWindowRequest::Team { auto_app_id, .. } = &mut self.request else {
|
|
| 554 |
+ let SettingsWindowRequest::Team {
|
|
| 555 |
+ parent, |
|
| 556 |
+ auto_app_id, |
|
| 557 |
+ .. |
|
| 558 |
+ } = &mut self.request |
|
| 559 |
+ else {
|
|
| 135 | 560 |
return; |
| 136 | 561 |
}; |
| 137 | 562 |
|
| 138 | 563 |
*auto_app_id = !*auto_app_id; |
| 564 |
+ let auto_app_id = *auto_app_id; |
|
| 139 | 565 |
self.app_id_picker_open = false; |
| 566 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 567 |
+ view.set_auto_app_id_from_settings(auto_app_id, cx); |
|
| 568 |
+ }); |
|
| 140 | 569 |
cx.notify(); |
| 141 | 570 |
} |
| 142 | 571 |
|
| ... | ... |
@@ -167,6 +596,7 @@ impl SettingsWindow {
|
| 167 | 596 |
) {
|
| 168 | 597 |
window.focus(&self.focus_handle, cx); |
| 169 | 598 |
let SettingsWindowRequest::Team {
|
| 599 |
+ parent, |
|
| 170 | 600 |
teams, |
| 171 | 601 |
selected_team, |
| 172 | 602 |
selected_app_id, |
| ... | ... |
@@ -183,6 +613,9 @@ impl SettingsWindow {
|
| 183 | 613 |
if index < team.app_ids.len() {
|
| 184 | 614 |
*selected_app_id = index; |
| 185 | 615 |
self.app_id_picker_open = false; |
| 616 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 617 |
+ view.select_app_id_from_settings(index, cx); |
|
| 618 |
+ }); |
|
| 186 | 619 |
cx.notify(); |
| 187 | 620 |
} |
| 188 | 621 |
} |
| ... | ... |
@@ -216,6 +649,20 @@ impl SettingsWindow {
|
| 216 | 649 |
} |
| 217 | 650 |
} |
| 218 | 651 |
|
| 652 |
+ fn log_out_developer_account( |
|
| 653 |
+ &mut self, |
|
| 654 |
+ _: &ClickEvent, |
|
| 655 |
+ window: &mut Window, |
|
| 656 |
+ cx: &mut Context<Self>, |
|
| 657 |
+ ) {
|
|
| 658 |
+ window.focus(&self.focus_handle, cx); |
|
| 659 |
+ if matches!(self.request, SettingsWindowRequest::Team { .. }) {
|
|
| 660 |
+ self.team_picker_open = false; |
|
| 661 |
+ self.app_id_picker_open = false; |
|
| 662 |
+ cx.notify(); |
|
| 663 |
+ } |
|
| 664 |
+ } |
|
| 665 |
+ |
|
| 219 | 666 |
fn select_adi_backend( |
| 220 | 667 |
&mut self, |
| 221 | 668 |
index: usize, |
| ... | ... |
@@ -224,6 +671,9 @@ impl SettingsWindow {
|
| 224 | 671 |
cx: &mut Context<Self>, |
| 225 | 672 |
) {
|
| 226 | 673 |
window.focus(&self.focus_handle, cx); |
| 674 |
+ if self.adi_operation.is_some() {
|
|
| 675 |
+ return; |
|
| 676 |
+ } |
|
| 227 | 677 |
let SettingsWindowRequest::AdiSettings {
|
| 228 | 678 |
parent, |
| 229 | 679 |
backends, |
| ... | ... |
@@ -241,10 +691,7 @@ impl SettingsWindow {
|
| 241 | 691 |
*selected_backend = index; |
| 242 | 692 |
self.adi_backend_picker_open = false; |
| 243 | 693 |
let _ = parent.update(cx, |view, cx| {
|
| 244 |
- if index < view.adi_backends.len() {
|
|
| 245 |
- view.selected_adi_backend = index; |
|
| 246 |
- cx.notify(); |
|
| 247 |
- } |
|
| 694 |
+ view.select_adi_backend_from_settings(index, cx); |
|
| 248 | 695 |
}); |
| 249 | 696 |
cx.notify(); |
| 250 | 697 |
} |
| ... | ... |
@@ -256,1479 +703,1406 @@ impl SettingsWindow {
|
| 256 | 703 |
cx: &mut Context<Self>, |
| 257 | 704 |
) {
|
| 258 | 705 |
window.focus(&self.focus_handle, cx); |
| 706 |
+ if self.adi_operation.is_some() {
|
|
| 707 |
+ return; |
|
| 708 |
+ } |
|
| 259 | 709 |
if matches!(self.request, SettingsWindowRequest::AdiSettings { .. }) {
|
| 260 | 710 |
self.adi_backend_picker_open = !self.adi_backend_picker_open; |
| 261 | 711 |
cx.notify(); |
| 262 | 712 |
} |
| 263 | 713 |
} |
| 264 | 714 |
|
| 265 |
- fn repair_adi_backend(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 715 |
+ fn open_data_folder(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 266 | 716 |
window.focus(&self.focus_handle, cx); |
| 267 |
- cx.notify(); |
|
| 717 |
+ self.adi_backend_picker_open = false; |
|
| 718 |
+ if let Err(error) = open_app_data_folder() {
|
|
| 719 |
+ eprintln!("{error}");
|
|
| 268 | 720 |
} |
| 269 |
- |
|
| 270 |
- fn edit_machine_identity( |
|
| 271 |
- &mut self, |
|
| 272 |
- _: &ClickEvent, |
|
| 273 |
- window: &mut Window, |
|
| 274 |
- cx: &mut Context<Self>, |
|
| 275 |
- ) {
|
|
| 276 |
- window.focus(&self.focus_handle, cx); |
|
| 277 | 721 |
cx.notify(); |
| 278 | 722 |
} |
| 279 | 723 |
|
| 280 |
- fn edit_app_icon(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 724 |
+ fn repair_adi_backend(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 281 | 725 |
window.focus(&self.focus_handle, cx); |
| 282 |
- cx.notify(); |
|
| 726 |
+ if self.adi_operation.is_some() {
|
|
| 727 |
+ return; |
|
| 283 | 728 |
} |
| 729 |
+ let Some((kind, _, _)) = self.selected_adi_backend_context() else {
|
|
| 730 |
+ return; |
|
| 731 |
+ }; |
|
| 284 | 732 |
|
| 285 |
- fn edit_app_detail(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 286 |
- window.focus(&self.focus_handle, cx); |
|
| 733 |
+ if kind != AdiBackendKind::AndroidCoreAdi {
|
|
| 287 | 734 |
cx.notify(); |
| 735 |
+ return; |
|
| 288 | 736 |
} |
| 289 | 737 |
|
| 290 |
- fn revert_app_detail(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 291 |
- window.focus(&self.focus_handle, cx); |
|
| 292 |
- cx.notify(); |
|
| 738 |
+ self.adi_backend_picker_open = false; |
|
| 739 |
+ self.adi_operation = Some(AdiOperationState::DownloadingCoreAdi {
|
|
| 740 |
+ downloaded_bytes: 0, |
|
| 741 |
+ total_bytes: None, |
|
| 742 |
+ }); |
|
| 743 |
+ let (progress_sender, mut progress_receiver) = mpsc::unbounded(); |
|
| 744 |
+ cx.spawn(async move |settings, cx| {
|
|
| 745 |
+ while let Some(event) = progress_receiver.next().await {
|
|
| 746 |
+ let _ = settings.update(cx, |settings, cx| {
|
|
| 747 |
+ settings.set_coreadi_install_event(event, cx); |
|
| 748 |
+ }); |
|
| 293 | 749 |
} |
| 750 |
+ }) |
|
| 751 |
+ .detach(); |
|
| 752 |
+ cx.spawn(async move |settings, cx| {
|
|
| 753 |
+ let result = cx |
|
| 754 |
+ .background_spawn(async move {
|
|
| 755 |
+ adi_services::download_and_install_coreadi(move |event| {
|
|
| 756 |
+ let _ = progress_sender.unbounded_send(event); |
|
| 757 |
+ }) |
|
| 758 |
+ }) |
|
| 759 |
+ .await; |
|
| 294 | 760 |
|
| 295 |
- fn add_entitlement(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 296 |
- window.focus(&self.focus_handle, cx); |
|
| 297 |
- cx.notify(); |
|
| 761 |
+ let _ = settings.update(cx, |settings, cx| match result {
|
|
| 762 |
+ Ok(_) => {
|
|
| 763 |
+ settings.adi_operation = None; |
|
| 764 |
+ settings.refresh_adi_backend_options(Some(AdiBackendKind::AndroidCoreAdi), cx) |
|
| 298 | 765 |
} |
| 299 |
- |
|
| 300 |
- fn remove_entitlement(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 301 |
- window.focus(&self.focus_handle, cx); |
|
| 766 |
+ Err(error) => {
|
|
| 767 |
+ settings.adi_operation = None; |
|
| 768 |
+ settings.set_selected_adi_provisioning_state( |
|
| 769 |
+ AdiProvisioningState::Error(error.into()), |
|
| 770 |
+ cx, |
|
| 771 |
+ ); |
|
| 772 |
+ } |
|
| 773 |
+ }); |
|
| 774 |
+ }) |
|
| 775 |
+ .detach(); |
|
| 302 | 776 |
cx.notify(); |
| 303 | 777 |
} |
| 304 | 778 |
|
| 305 |
- fn revert_entitlements(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 779 |
+ fn select_coreadi_apk(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 306 | 780 |
window.focus(&self.focus_handle, cx); |
| 307 |
- cx.notify(); |
|
| 781 |
+ if self.adi_operation.is_some() {
|
|
| 782 |
+ return; |
|
| 308 | 783 |
} |
| 784 |
+ let receiver = cx.prompt_for_paths(PathPromptOptions {
|
|
| 785 |
+ files: true, |
|
| 786 |
+ directories: false, |
|
| 787 |
+ multiple: false, |
|
| 788 |
+ prompt: Some("Select APK".into()),
|
|
| 789 |
+ }); |
|
| 790 |
+ |
|
| 791 |
+ cx.spawn(async move |settings, cx| {
|
|
| 792 |
+ let paths = match receiver.await {
|
|
| 793 |
+ Ok(Ok(Some(paths))) => paths, |
|
| 794 |
+ Ok(Ok(None)) | Err(_) => return, |
|
| 795 |
+ Ok(Err(error)) => {
|
|
| 796 |
+ let _ = settings.update(cx, |settings, cx| {
|
|
| 797 |
+ settings.set_selected_adi_provisioning_state( |
|
| 798 |
+ AdiProvisioningState::Error(error.to_string().into()), |
|
| 799 |
+ cx, |
|
| 800 |
+ ); |
|
| 801 |
+ }); |
|
| 802 |
+ return; |
|
| 309 | 803 |
} |
| 804 |
+ }; |
|
| 310 | 805 |
|
| 311 |
-impl Render for SettingsWindow {
|
|
| 312 |
- fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
| 313 |
- let team_picker_open = self.team_picker_open; |
|
| 314 |
- let app_id_picker_open = self.app_id_picker_open; |
|
| 315 |
- let adi_backend_picker_open = self.adi_backend_picker_open; |
|
| 806 |
+ let Some(apk_path) = paths.into_iter().next() else {
|
|
| 807 |
+ return; |
|
| 808 |
+ }; |
|
| 316 | 809 |
|
| 317 |
- match &mut self.request {
|
|
| 318 |
- SettingsWindowRequest::Team {
|
|
| 319 |
- teams, |
|
| 320 |
- selected_team, |
|
| 321 |
- auto_app_id, |
|
| 322 |
- selected_app_id, |
|
| 323 |
- .. |
|
| 324 |
- } => {
|
|
| 325 |
- let selected = *selected_team; |
|
| 326 |
- let auto_app_id = *auto_app_id; |
|
| 327 |
- let selected_app_id = *selected_app_id; |
|
| 328 |
- let team = teams.get(selected); |
|
| 329 |
- let mut content = div() |
|
| 330 |
- .flex() |
|
| 331 |
- .flex_col() |
|
| 332 |
- .gap_4() |
|
| 333 |
- .when_some(team, |this, team| {
|
|
| 334 |
- this.child(developer_settings_section( |
|
| 335 |
- "Developer Team", |
|
| 336 |
- "Team used for certificates, profiles, and signing identities.", |
|
| 337 |
- div() |
|
| 338 |
- .flex() |
|
| 339 |
- .flex_col() |
|
| 340 |
- .gap_3() |
|
| 341 |
- .child(combo_with_popover( |
|
| 342 |
- "team-popover-scroll", |
|
| 343 |
- combo_button( |
|
| 344 |
- team.identifier.clone(), |
|
| 345 |
- team.name.clone(), |
|
| 346 |
- developer_account_title(&team.role), |
|
| 347 |
- team_picker_open, |
|
| 348 |
- ) |
|
| 349 |
- .id("settings-team-combobox")
|
|
| 350 |
- .on_click(cx.listener(Self::toggle_team_picker)), |
|
| 351 |
- team_picker_open, |
|
| 352 |
- dropdown_list((0..teams.len()).map(|index| {
|
|
| 353 |
- settings_team_row(&teams[index], index == selected) |
|
| 354 |
- .id(("settings-team-row", index))
|
|
| 355 |
- .on_click(cx.listener( |
|
| 356 |
- move |this, event, window, cx| {
|
|
| 357 |
- this.select_team(index, event, window, cx) |
|
| 358 |
- }, |
|
| 359 |
- )) |
|
| 360 |
- })) |
|
| 361 |
- .id("settings-team-options"),
|
|
| 362 |
- )) |
|
| 363 |
- .child(properties_list(vec![ |
|
| 364 |
- ("Name", team.name.to_string()),
|
|
| 365 |
- ("Team ID", team.identifier.to_string()),
|
|
| 366 |
- ("Role", team.role.to_string()),
|
|
| 367 |
- ])), |
|
| 368 |
- )) |
|
| 810 |
+ let _ = settings.update(cx, |settings, cx| {
|
|
| 811 |
+ settings.adi_operation = Some(AdiOperationState::InstallingCoreAdi); |
|
| 812 |
+ cx.notify(); |
|
| 369 | 813 |
}); |
| 370 |
- |
|
| 371 |
- if let Some(team) = team {
|
|
| 372 |
- content = content.child(app_id_section( |
|
| 373 |
- team, |
|
| 374 |
- auto_app_id, |
|
| 375 |
- selected_app_id, |
|
| 376 |
- app_id_picker_open, |
|
| 814 |
+ let result = cx |
|
| 815 |
+ .background_spawn(async move { adi_services::install_coreadi_from_apk(&apk_path) })
|
|
| 816 |
+ .await; |
|
| 817 |
+ let _ = settings.update(cx, |settings, cx| match result {
|
|
| 818 |
+ Ok(_) => {
|
|
| 819 |
+ settings.adi_operation = None; |
|
| 820 |
+ settings.refresh_adi_backend_options(Some(AdiBackendKind::AndroidCoreAdi), cx) |
|
| 821 |
+ } |
|
| 822 |
+ Err(error) => {
|
|
| 823 |
+ settings.adi_operation = None; |
|
| 824 |
+ settings.set_selected_adi_provisioning_state( |
|
| 825 |
+ AdiProvisioningState::Error(error.into()), |
|
| 377 | 826 |
cx, |
| 378 |
- )); |
|
| 827 |
+ ); |
|
| 828 |
+ } |
|
| 829 |
+ }); |
|
| 830 |
+ }) |
|
| 831 |
+ .detach(); |
|
| 379 | 832 |
} |
| 380 | 833 |
|
| 381 |
- settings_window_shell() |
|
| 382 |
- .track_focus(&self.focus_handle) |
|
| 383 |
- .gap_4() |
|
| 384 |
- .child(settings_window_header_with_action( |
|
| 385 |
- settings_window_header( |
|
| 386 |
- "Developer Settings", |
|
| 387 |
- "Configure developer-team resources used during signing.", |
|
| 388 |
- ), |
|
| 389 |
- team_refresh_button(cx), |
|
| 390 |
- )) |
|
| 391 |
- .child(scroll_panel( |
|
| 392 |
- "team-settings-scroll", |
|
| 393 |
- &self.scroll_handle, |
|
| 394 |
- content, |
|
| 395 |
- )) |
|
| 834 |
+ fn erase_adi_backend_provisioning( |
|
| 835 |
+ &mut self, |
|
| 836 |
+ _: &ClickEvent, |
|
| 837 |
+ window: &mut Window, |
|
| 838 |
+ cx: &mut Context<Self>, |
|
| 839 |
+ ) {
|
|
| 840 |
+ window.focus(&self.focus_handle, cx); |
|
| 841 |
+ if self.adi_operation.is_some() {
|
|
| 842 |
+ return; |
|
| 396 | 843 |
} |
| 397 |
- SettingsWindowRequest::AppSettings {
|
|
| 398 |
- app, |
|
| 399 |
- enabled_patches, |
|
| 400 |
- .. |
|
| 401 |
- } => {
|
|
| 402 |
- let patch_summary = enabled_patch_summary(app, enabled_patches); |
|
| 844 |
+ let Some((kind, _machine_identity, android_adi_identifier)) = |
|
| 845 |
+ self.selected_adi_backend_context() |
|
| 846 |
+ else {
|
|
| 847 |
+ return; |
|
| 848 |
+ }; |
|
| 403 | 849 |
|
| 404 |
- settings_window_shell() |
|
| 405 |
- .track_focus(&self.focus_handle) |
|
| 406 |
- .child(scroll_panel( |
|
| 407 |
- "app-settings-scroll", |
|
| 408 |
- &self.scroll_handle, |
|
| 409 |
- div() |
|
| 410 |
- .flex() |
|
| 411 |
- .flex_col() |
|
| 412 |
- .gap_4() |
|
| 413 |
- .child(settings_window_header( |
|
| 414 |
- "App Settings", |
|
| 415 |
- "Review metadata and entitlements before signing.", |
|
| 416 |
- )) |
|
| 417 |
- .child( |
|
| 418 |
- div() |
|
| 419 |
- .flex() |
|
| 420 |
- .gap_3() |
|
| 421 |
- .p_3() |
|
| 422 |
- .rounded_md() |
|
| 423 |
- .border_1() |
|
| 424 |
- .border_color(rgb(0xd8e0df)) |
|
| 425 |
- .bg(rgb(0xffffff)) |
|
| 426 |
- .child(app_icon_editor(app, cx)) |
|
| 427 |
- .child( |
|
| 428 |
- div() |
|
| 429 |
- .min_w_0() |
|
| 430 |
- .flex() |
|
| 431 |
- .flex_1() |
|
| 432 |
- .flex_col() |
|
| 433 |
- .gap_2() |
|
| 434 |
- .child(app_detail_row( |
|
| 435 |
- "Name", |
|
| 436 |
- app.name.to_string(), |
|
| 437 |
- true, |
|
| 438 |
- cx, |
|
| 439 |
- )) |
|
| 440 |
- .child(app_detail_row( |
|
| 441 |
- "Bundle ID", |
|
| 442 |
- app.bundle_id.to_string(), |
|
| 443 |
- true, |
|
| 444 |
- cx, |
|
| 445 |
- )) |
|
| 446 |
- .child(app_detail_row( |
|
| 447 |
- "Version", |
|
| 448 |
- app.version.to_string(), |
|
| 449 |
- true, |
|
| 450 |
- cx, |
|
| 451 |
- )) |
|
| 452 |
- .child(app_detail_row( |
|
| 453 |
- "Build", |
|
| 454 |
- app.build.to_string(), |
|
| 455 |
- true, |
|
| 850 |
+ match adi_services::erase_provisioning(kind, &android_adi_identifier) {
|
|
| 851 |
+ Ok(()) => self.refresh_adi_backend_options(Some(kind), cx), |
|
| 852 |
+ Err(error) => self |
|
| 853 |
+ .set_selected_adi_provisioning_state(AdiProvisioningState::Error(error.into()), cx), |
|
| 854 |
+ } |
|
| 855 |
+ } |
|
| 856 |
+ |
|
| 857 |
+ fn provision_adi_backend( |
|
| 858 |
+ &mut self, |
|
| 859 |
+ _: &ClickEvent, |
|
| 860 |
+ window: &mut Window, |
|
| 861 |
+ cx: &mut Context<Self>, |
|
| 862 |
+ ) {
|
|
| 863 |
+ window.focus(&self.focus_handle, cx); |
|
| 864 |
+ if self.adi_operation.is_some() {
|
|
| 865 |
+ return; |
|
| 866 |
+ } |
|
| 867 |
+ let Some((kind, machine_identity, android_adi_identifier)) = |
|
| 868 |
+ self.selected_adi_backend_context() |
|
| 869 |
+ else {
|
|
| 870 |
+ return; |
|
| 871 |
+ }; |
|
| 872 |
+ |
|
| 873 |
+ self.adi_backend_picker_open = false; |
|
| 874 |
+ self.adi_operation = Some(AdiOperationState::Provisioning); |
|
| 875 |
+ cx.spawn(async move |settings, cx| {
|
|
| 876 |
+ let result = cx |
|
| 877 |
+ .background_spawn(async move {
|
|
| 878 |
+ adi_services::provision(kind, &machine_identity, &android_adi_identifier) |
|
| 879 |
+ }) |
|
| 880 |
+ .await; |
|
| 881 |
+ let _ = settings.update(cx, |settings, cx| {
|
|
| 882 |
+ settings.adi_operation = None; |
|
| 883 |
+ match result {
|
|
| 884 |
+ Ok(()) => settings.refresh_adi_backend_options(Some(kind), cx), |
|
| 885 |
+ Err(error) => settings.set_selected_adi_provisioning_state( |
|
| 886 |
+ AdiProvisioningState::Error(error.into()), |
|
| 456 | 887 |
cx, |
| 457 |
- )), |
|
| 458 | 888 |
), |
| 459 |
- ) |
|
| 460 |
- .child( |
|
| 461 |
- div() |
|
| 462 |
- .flex() |
|
| 463 |
- .flex_col() |
|
| 464 |
- .gap_2() |
|
| 465 |
- .p_3() |
|
| 466 |
- .rounded_md() |
|
| 467 |
- .border_1() |
|
| 468 |
- .border_color(rgb(0xd8e0df)) |
|
| 469 |
- .bg(rgb(0xffffff)) |
|
| 470 |
- .child(app_detail_row("IPA", app.path.to_string(), false, cx))
|
|
| 471 |
- .child(app_detail_row( |
|
| 472 |
- "Executable", |
|
| 473 |
- app.name.to_string().replace(' ', ""),
|
|
| 474 |
- true, |
|
| 475 |
- cx, |
|
| 476 |
- )) |
|
| 477 |
- .child(app_detail_row( |
|
| 478 |
- "Minimum OS", |
|
| 479 |
- "iOS 15.0".to_string(), |
|
| 480 |
- true, |
|
| 481 |
- cx, |
|
| 482 |
- )) |
|
| 483 |
- .child(app_detail_row( |
|
| 484 |
- "Supported devices", |
|
| 485 |
- "iPhone, iPad".to_string(), |
|
| 486 |
- true, |
|
| 487 |
- cx, |
|
| 488 |
- )) |
|
| 489 |
- .child(app_detail_row("Patches", patch_summary, false, cx)),
|
|
| 490 |
- ) |
|
| 491 |
- .child(entitlements_table(app, cx)), |
|
| 492 |
- )) |
|
| 493 | 889 |
} |
| 494 |
- SettingsWindowRequest::AdiSettings {
|
|
| 890 |
+ }); |
|
| 891 |
+ }) |
|
| 892 |
+ .detach(); |
|
| 893 |
+ cx.notify(); |
|
| 894 |
+ } |
|
| 895 |
+ |
|
| 896 |
+ fn selected_adi_backend_context(&self) -> Option<(AdiBackendKind, MachineIdentity, String)> {
|
|
| 897 |
+ let SettingsWindowRequest::AdiSettings {
|
|
| 495 | 898 |
backends, |
| 496 | 899 |
selected_backend, |
| 497 | 900 |
machine_identity, |
| 901 |
+ android_device_identity, |
|
| 902 |
+ android_adi_identifier, |
|
| 498 | 903 |
.. |
| 499 |
- } => {
|
|
| 500 |
- let selected = *selected_backend; |
|
| 501 |
- let backend = backends.get(selected); |
|
| 502 |
- let editable_identity = backend |
|
| 503 |
- .map(|backend| backend.editable_identity) |
|
| 504 |
- .unwrap_or(false); |
|
| 904 |
+ } = &self.request |
|
| 905 |
+ else {
|
|
| 906 |
+ return None; |
|
| 907 |
+ }; |
|
| 505 | 908 |
|
| 506 |
- let mut content = div() |
|
| 507 |
- .flex() |
|
| 508 |
- .flex_col() |
|
| 509 |
- .gap_4() |
|
| 510 |
- .child(settings_window_header( |
|
| 511 |
- "Settings", |
|
| 512 |
- "Choose the ADI backend and review the machine identity it uses.", |
|
| 513 |
- )) |
|
| 514 |
- .child( |
|
| 515 |
- div() |
|
| 516 |
- .flex() |
|
| 517 |
- .flex_col() |
|
| 518 |
- .gap_2() |
|
| 519 |
- .child( |
|
| 520 |
- div() |
|
| 521 |
- .text_xs() |
|
| 522 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 523 |
- .text_color(rgb(0x66767c)) |
|
| 524 |
- .child("ADI Backend"),
|
|
| 525 |
- ) |
|
| 526 |
- .when_some(backend, |this, backend| {
|
|
| 527 |
- this.child(settings_backend_combo_with_popover( |
|
| 528 |
- settings_adi_backend_combo_button( |
|
| 529 |
- backend, |
|
| 530 |
- adi_backend_picker_open, |
|
| 531 |
- ) |
|
| 532 |
- .id("adi-backend-combobox")
|
|
| 533 |
- .on_click(cx.listener(Self::toggle_adi_backend_picker)), |
|
| 534 |
- adi_backend_picker_open, |
|
| 535 |
- dropdown_list((0..backends.len()).map(|index| {
|
|
| 536 |
- settings_adi_backend_option( |
|
| 537 |
- &backends[index], |
|
| 538 |
- index == selected, |
|
| 539 |
- ) |
|
| 540 |
- .id(("adi-backend-option", index))
|
|
| 541 |
- .on_click( |
|
| 542 |
- cx.listener(move |this, event, window, cx| {
|
|
| 543 |
- this.select_adi_backend(index, event, window, cx) |
|
| 544 |
- }), |
|
| 909 |
+ backends.get(*selected_backend).map(|backend| {
|
|
| 910 |
+ let machine_identity = match backend.kind {
|
|
| 911 |
+ AdiBackendKind::AndroidCoreAdi => android_device_identity.clone(), |
|
| 912 |
+ AdiBackendKind::SystemAdid | AdiBackendKind::WindowsCoreAdi => {
|
|
| 913 |
+ machine_identity.clone() |
|
| 914 |
+ } |
|
| 915 |
+ }; |
|
| 916 |
+ ( |
|
| 917 |
+ backend.kind, |
|
| 918 |
+ machine_identity, |
|
| 919 |
+ android_adi_identifier.clone(), |
|
| 545 | 920 |
) |
| 546 |
- })) |
|
| 547 |
- .id("adi-backend-options"),
|
|
| 548 |
- )) |
|
| 549 |
- }), |
|
| 550 |
- ); |
|
| 921 |
+ }) |
|
| 922 |
+ } |
|
| 923 |
+ |
|
| 924 |
+ fn refresh_adi_backend_options( |
|
| 925 |
+ &mut self, |
|
| 926 |
+ preferred_kind: Option<AdiBackendKind>, |
|
| 927 |
+ cx: &mut Context<Self>, |
|
| 928 |
+ ) {
|
|
| 929 |
+ let SettingsWindowRequest::AdiSettings {
|
|
| 930 |
+ parent, |
|
| 931 |
+ backends, |
|
| 932 |
+ selected_backend, |
|
| 933 |
+ android_adi_identifier, |
|
| 934 |
+ .. |
|
| 935 |
+ } = &mut self.request |
|
| 936 |
+ else {
|
|
| 937 |
+ return; |
|
| 938 |
+ }; |
|
| 939 |
+ |
|
| 940 |
+ let selected_kind = |
|
| 941 |
+ preferred_kind.or_else(|| backends.get(*selected_backend).map(|backend| backend.kind)); |
|
| 942 |
+ let refreshed = adi_services::available_backends(android_adi_identifier); |
|
| 943 |
+ let selected = selected_kind |
|
| 944 |
+ .and_then(|kind| refreshed.iter().position(|backend| backend.kind == kind)) |
|
| 945 |
+ .unwrap_or_else(|| adi_services::default_backend(&refreshed)); |
|
| 946 |
+ |
|
| 947 |
+ *backends = refreshed.clone(); |
|
| 948 |
+ *selected_backend = selected; |
|
| 551 | 949 |
|
| 552 |
- if let Some(backend) = backend {
|
|
| 553 |
- content = content.child(adi_backend_status_section(backend, cx)); |
|
| 950 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 951 |
+ view.replace_adi_backends_from_settings(refreshed, selected, true, cx); |
|
| 952 |
+ }); |
|
| 953 |
+ cx.notify(); |
|
| 554 | 954 |
} |
| 555 | 955 |
|
| 556 |
- content = content.child(machine_identity_section( |
|
| 557 |
- machine_identity, |
|
| 558 |
- editable_identity, |
|
| 559 |
- cx, |
|
| 560 |
- )); |
|
| 956 |
+ fn set_selected_adi_provisioning_state( |
|
| 957 |
+ &mut self, |
|
| 958 |
+ state: AdiProvisioningState, |
|
| 959 |
+ cx: &mut Context<Self>, |
|
| 960 |
+ ) {
|
|
| 961 |
+ let SettingsWindowRequest::AdiSettings {
|
|
| 962 |
+ parent, |
|
| 963 |
+ backends, |
|
| 964 |
+ selected_backend, |
|
| 965 |
+ .. |
|
| 966 |
+ } = &mut self.request |
|
| 967 |
+ else {
|
|
| 968 |
+ return; |
|
| 969 |
+ }; |
|
| 561 | 970 |
|
| 562 |
- settings_window_shell() |
|
| 563 |
- .track_focus(&self.focus_handle) |
|
| 564 |
- .child(scroll_panel( |
|
| 565 |
- "adi-settings-scroll", |
|
| 566 |
- &self.scroll_handle, |
|
| 567 |
- content, |
|
| 568 |
- )) |
|
| 971 |
+ if let Some(backend) = backends.get_mut(*selected_backend) {
|
|
| 972 |
+ backend.provisioning_state = state.clone(); |
|
| 973 |
+ } |
|
| 974 |
+ |
|
| 975 |
+ let backends = backends.clone(); |
|
| 976 |
+ let selected = *selected_backend; |
|
| 977 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 978 |
+ view.replace_adi_backends_from_settings(backends, selected, false, cx); |
|
| 979 |
+ }); |
|
| 980 |
+ cx.notify(); |
|
| 569 | 981 |
} |
| 982 |
+ |
|
| 983 |
+ fn set_coreadi_install_event(&mut self, event: CoreAdiInstallEvent, cx: &mut Context<Self>) {
|
|
| 984 |
+ self.adi_operation = Some(match event {
|
|
| 985 |
+ CoreAdiInstallEvent::Downloading(progress) => AdiOperationState::DownloadingCoreAdi {
|
|
| 986 |
+ downloaded_bytes: progress.downloaded_bytes, |
|
| 987 |
+ total_bytes: progress.total_bytes, |
|
| 988 |
+ }, |
|
| 989 |
+ CoreAdiInstallEvent::Installing => AdiOperationState::InstallingCoreAdi, |
|
| 990 |
+ }); |
|
| 991 |
+ cx.notify(); |
|
| 570 | 992 |
} |
| 993 |
+ |
|
| 994 |
+ fn begin_machine_identity_edit( |
|
| 995 |
+ &mut self, |
|
| 996 |
+ field: MachineIdentityField, |
|
| 997 |
+ _: &ClickEvent, |
|
| 998 |
+ window: &mut Window, |
|
| 999 |
+ cx: &mut Context<Self>, |
|
| 1000 |
+ ) {
|
|
| 1001 |
+ window.focus(&self.focus_handle, cx); |
|
| 1002 |
+ if self.adi_operation.is_some() {
|
|
| 1003 |
+ return; |
|
| 571 | 1004 |
} |
| 1005 |
+ let Some(value) = self.editable_machine_identity_value(field).cloned() else {
|
|
| 1006 |
+ return; |
|
| 1007 |
+ }; |
|
| 1008 |
+ let input = cx.new(|cx| EditLine::new(value, cx)); |
|
| 1009 |
+ self.machine_identity_edit = Some(MachineIdentityEdit {
|
|
| 1010 |
+ field, |
|
| 1011 |
+ input: input.clone(), |
|
| 1012 |
+ }); |
|
| 1013 |
+ input.update(cx, |input, cx| input.focus(window, cx)); |
|
| 1014 |
+ cx.notify(); |
|
| 572 | 1015 |
} |
| 573 | 1016 |
|
| 574 |
-pub(crate) fn show_or_open_settings_window( |
|
| 575 |
- handle: Option<WindowHandle<SettingsWindow>>, |
|
| 576 |
- request: SettingsWindowRequest, |
|
| 577 |
- width: f32, |
|
| 578 |
- height: f32, |
|
| 579 |
- cx: &mut App, |
|
| 580 |
-) -> WindowHandle<SettingsWindow> {
|
|
| 581 |
- if let Some(handle) = handle {
|
|
| 582 |
- if handle |
|
| 583 |
- .update(cx, |settings, window, cx| {
|
|
| 584 |
- settings.show_request(request.clone(), window, cx); |
|
| 585 |
- }) |
|
| 586 |
- .is_ok() |
|
| 587 |
- {
|
|
| 588 |
- return handle; |
|
| 1017 |
+ fn save_machine_identity_edit_from_button( |
|
| 1018 |
+ &mut self, |
|
| 1019 |
+ _: &ClickEvent, |
|
| 1020 |
+ window: &mut Window, |
|
| 1021 |
+ cx: &mut Context<Self>, |
|
| 1022 |
+ ) {
|
|
| 1023 |
+ window.focus(&self.focus_handle, cx); |
|
| 1024 |
+ self.save_machine_identity_edit(cx); |
|
| 589 | 1025 |
} |
| 1026 |
+ |
|
| 1027 |
+ fn cancel_machine_identity_edit( |
|
| 1028 |
+ &mut self, |
|
| 1029 |
+ _: &ClickEvent, |
|
| 1030 |
+ window: &mut Window, |
|
| 1031 |
+ cx: &mut Context<Self>, |
|
| 1032 |
+ ) {
|
|
| 1033 |
+ window.focus(&self.focus_handle, cx); |
|
| 1034 |
+ self.machine_identity_edit = None; |
|
| 1035 |
+ cx.notify(); |
|
| 590 | 1036 |
} |
| 591 | 1037 |
|
| 592 |
- open_settings_window(request, width, height, cx) |
|
| 1038 |
+ fn handle_machine_identity_editor_key( |
|
| 1039 |
+ &mut self, |
|
| 1040 |
+ event: &KeyDownEvent, |
|
| 1041 |
+ window: &mut Window, |
|
| 1042 |
+ cx: &mut Context<Self>, |
|
| 1043 |
+ ) {
|
|
| 1044 |
+ if self.machine_identity_edit.is_none() {
|
|
| 1045 |
+ return; |
|
| 593 | 1046 |
} |
| 594 | 1047 |
|
| 595 |
-fn open_settings_window( |
|
| 596 |
- request: SettingsWindowRequest, |
|
| 597 |
- width: f32, |
|
| 598 |
- height: f32, |
|
| 599 |
- cx: &mut App, |
|
| 600 |
-) -> WindowHandle<SettingsWindow> {
|
|
| 601 |
- let window_size = size(px(width), px(height)); |
|
| 602 |
- let bounds = Bounds::centered(None, window_size, cx); |
|
| 603 |
- let title = request.title(); |
|
| 1048 |
+ match event.keystroke.key.as_str() {
|
|
| 1049 |
+ "enter" => {
|
|
| 1050 |
+ cx.stop_propagation(); |
|
| 1051 |
+ window.prevent_default(); |
|
| 1052 |
+ self.save_machine_identity_edit(cx); |
|
| 1053 |
+ } |
|
| 1054 |
+ "escape" => {
|
|
| 1055 |
+ cx.stop_propagation(); |
|
| 1056 |
+ window.prevent_default(); |
|
| 1057 |
+ self.machine_identity_edit = None; |
|
| 1058 |
+ cx.notify(); |
|
| 1059 |
+ } |
|
| 1060 |
+ _ => {}
|
|
| 1061 |
+ } |
|
| 1062 |
+ } |
|
| 604 | 1063 |
|
| 605 |
- cx.open_window( |
|
| 606 |
- WindowOptions {
|
|
| 607 |
- window_bounds: Some(WindowBounds::Windowed(bounds)), |
|
| 608 |
- window_min_size: Some(window_size), |
|
| 609 |
- is_resizable: true, |
|
| 610 |
- is_minimizable: false, |
|
| 611 |
- kind: WindowKind::Floating, |
|
| 612 |
- ..Default::default() |
|
| 613 |
- }, |
|
| 614 |
- |window, cx| {
|
|
| 615 |
- window.set_window_title(title); |
|
| 616 |
- cx.new(|cx| SettingsWindow::new(request, window, cx)) |
|
| 617 |
- }, |
|
| 618 |
- ) |
|
| 619 |
- .expect("failed to open settings window")
|
|
| 1064 |
+ fn editable_machine_identity_value( |
|
| 1065 |
+ &self, |
|
| 1066 |
+ field: MachineIdentityField, |
|
| 1067 |
+ ) -> Option<&SharedString> {
|
|
| 1068 |
+ let SettingsWindowRequest::AdiSettings {
|
|
| 1069 |
+ backends, |
|
| 1070 |
+ selected_backend, |
|
| 1071 |
+ android_device_identity, |
|
| 1072 |
+ .. |
|
| 1073 |
+ } = &self.request |
|
| 1074 |
+ else {
|
|
| 1075 |
+ return None; |
|
| 1076 |
+ }; |
|
| 1077 |
+ |
|
| 1078 |
+ let backend = backends.get(*selected_backend)?; |
|
| 1079 |
+ if backend.kind != AdiBackendKind::AndroidCoreAdi || !backend.editable_identity {
|
|
| 1080 |
+ return None; |
|
| 620 | 1081 |
} |
| 621 | 1082 |
|
| 622 |
-fn scroll_panel( |
|
| 623 |
- scroll_id: &'static str, |
|
| 624 |
- scroll_handle: &ScrollHandle, |
|
| 625 |
- content: impl IntoElement, |
|
| 626 |
-) -> gpui::Div {
|
|
| 627 |
- let viewport_height = scroll_handle.bounds().size.height.as_f32(); |
|
| 628 |
- let max_scroll = scroll_handle.max_offset().y.as_f32(); |
|
| 629 |
- let is_scrollable = viewport_height > 0. && max_scroll > 0.5; |
|
| 630 |
- let scroll_offset = (-scroll_handle.offset().y.as_f32()).clamp(0., max_scroll); |
|
| 631 |
- let content_height = viewport_height + max_scroll; |
|
| 632 |
- let thumb_height = if is_scrollable {
|
|
| 633 |
- ((viewport_height / content_height) * viewport_height).clamp(36., viewport_height) |
|
| 634 |
- } else {
|
|
| 635 |
- 0. |
|
| 636 |
- }; |
|
| 637 |
- let thumb_top = if is_scrollable && max_scroll > 0. {
|
|
| 638 |
- (scroll_offset / max_scroll) * (viewport_height - thumb_height) |
|
| 639 |
- } else {
|
|
| 640 |
- 0. |
|
| 1083 |
+ Some(field.value(android_device_identity)) |
|
| 1084 |
+ } |
|
| 1085 |
+ |
|
| 1086 |
+ fn save_machine_identity_edit(&mut self, cx: &mut Context<Self>) {
|
|
| 1087 |
+ let Some(edit) = self.machine_identity_edit.take() else {
|
|
| 1088 |
+ return; |
|
| 641 | 1089 |
}; |
| 642 | 1090 |
|
| 643 |
- div() |
|
| 644 |
- .min_h_0() |
|
| 645 |
- .flex_1() |
|
| 646 |
- .flex() |
|
| 647 |
- .gap_2() |
|
| 648 |
- .child( |
|
| 649 |
- div() |
|
| 650 |
- .id(scroll_id) |
|
| 651 |
- .min_w_0() |
|
| 652 |
- .h_full() |
|
| 653 |
- .flex_1() |
|
| 654 |
- .overflow_y_scroll() |
|
| 655 |
- .scrollbar_width(px(1.)) |
|
| 656 |
- .track_scroll(scroll_handle) |
|
| 657 |
- .child(content), |
|
| 658 |
- ) |
|
| 659 |
- .when(is_scrollable, |this| {
|
|
| 660 |
- this.child( |
|
| 661 |
- div() |
|
| 662 |
- .flex_none() |
|
| 663 |
- .w(px(6.)) |
|
| 664 |
- .h_full() |
|
| 665 |
- .rounded_full() |
|
| 666 |
- .bg(rgb(0xdde5e4)) |
|
| 667 |
- .flex() |
|
| 668 |
- .flex_col() |
|
| 669 |
- .child(div().flex_none().h(px(thumb_top))) |
|
| 670 |
- .child( |
|
| 671 |
- div() |
|
| 672 |
- .flex_none() |
|
| 673 |
- .w_full() |
|
| 674 |
- .h(px(thumb_height)) |
|
| 675 |
- .rounded_full() |
|
| 676 |
- .bg(rgb(0x789094)), |
|
| 677 |
- ) |
|
| 678 |
- .child(div().flex_1()), |
|
| 679 |
- ) |
|
| 680 |
- }) |
|
| 1091 |
+ let mut changed_identity = None; |
|
| 1092 |
+ let mut should_erase_provisioning = false; |
|
| 1093 |
+ let mut android_adi_identifier = String::new(); |
|
| 1094 |
+ let mut error = None; |
|
| 1095 |
+ |
|
| 1096 |
+ if let SettingsWindowRequest::AdiSettings {
|
|
| 1097 |
+ parent, |
|
| 1098 |
+ backends, |
|
| 1099 |
+ selected_backend, |
|
| 1100 |
+ android_device_identity, |
|
| 1101 |
+ android_adi_identifier: request_android_adi_identifier, |
|
| 1102 |
+ .. |
|
| 1103 |
+ } = &mut self.request |
|
| 1104 |
+ {
|
|
| 1105 |
+ let Some(backend) = backends.get(*selected_backend) else {
|
|
| 1106 |
+ cx.notify(); |
|
| 1107 |
+ return; |
|
| 1108 |
+ }; |
|
| 1109 |
+ if backend.kind != AdiBackendKind::AndroidCoreAdi || !backend.editable_identity {
|
|
| 1110 |
+ cx.notify(); |
|
| 1111 |
+ return; |
|
| 681 | 1112 |
} |
| 682 | 1113 |
|
| 683 |
-fn settings_window_shell() -> gpui::Div {
|
|
| 684 |
- div() |
|
| 685 |
- .size_full() |
|
| 686 |
- .p_5() |
|
| 687 |
- .flex() |
|
| 688 |
- .flex_col() |
|
| 689 |
- .bg(rgb(0xf4f6f4)) |
|
| 690 |
- .text_color(rgb(0x263238)) |
|
| 691 |
- .font_family(".SystemUIFont")
|
|
| 1114 |
+ let new_value = edit.input.read(cx).value().to_string(); |
|
| 1115 |
+ let old_value = edit.field.value(android_device_identity).to_string(); |
|
| 1116 |
+ if old_value != new_value {
|
|
| 1117 |
+ edit.field.set_value(android_device_identity, new_value); |
|
| 1118 |
+ changed_identity = Some((parent.clone(), android_device_identity.clone())); |
|
| 1119 |
+ should_erase_provisioning = matches!( |
|
| 1120 |
+ backend.provisioning_state, |
|
| 1121 |
+ AdiProvisioningState::Provisioned |
|
| 1122 |
+ ); |
|
| 1123 |
+ android_adi_identifier = request_android_adi_identifier.clone(); |
|
| 1124 |
+ } |
|
| 692 | 1125 |
} |
| 693 | 1126 |
|
| 694 |
-fn settings_window_header(title: &'static str, detail: &'static str) -> gpui::Div {
|
|
| 695 |
- div() |
|
| 696 |
- .flex() |
|
| 697 |
- .flex_col() |
|
| 698 |
- .gap_0p5() |
|
| 699 |
- .child( |
|
| 700 |
- div() |
|
| 701 |
- .text_sm() |
|
| 702 |
- .font_weight(FontWeight::BOLD) |
|
| 703 |
- .text_color(rgb(0x24333a)) |
|
| 704 |
- .child(title), |
|
| 1127 |
+ if let Some((parent, identity)) = changed_identity {
|
|
| 1128 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 1129 |
+ view.replace_android_device_identity_from_settings(identity, cx); |
|
| 1130 |
+ }); |
|
| 1131 |
+ |
|
| 1132 |
+ if should_erase_provisioning {
|
|
| 1133 |
+ error = adi_services::erase_provisioning( |
|
| 1134 |
+ AdiBackendKind::AndroidCoreAdi, |
|
| 1135 |
+ &android_adi_identifier, |
|
| 705 | 1136 |
) |
| 706 |
- .child(div().text_xs().text_color(rgb(0x6a7a81)).child(detail)) |
|
| 1137 |
+ .err(); |
|
| 707 | 1138 |
} |
| 708 | 1139 |
|
| 709 |
-fn settings_window_header_with_action( |
|
| 710 |
- header: impl IntoElement, |
|
| 711 |
- action: impl IntoElement, |
|
| 712 |
-) -> gpui::Div {
|
|
| 713 |
- div() |
|
| 714 |
- .flex() |
|
| 715 |
- .items_start() |
|
| 716 |
- .justify_between() |
|
| 717 |
- .gap_3() |
|
| 718 |
- .child(header) |
|
| 719 |
- .child(action) |
|
| 1140 |
+ self.refresh_adi_backend_options(Some(AdiBackendKind::AndroidCoreAdi), cx); |
|
| 1141 |
+ if let Some(error) = error {
|
|
| 1142 |
+ self.set_selected_adi_provisioning_state( |
|
| 1143 |
+ AdiProvisioningState::Error(error.into()), |
|
| 1144 |
+ cx, |
|
| 1145 |
+ ); |
|
| 720 | 1146 |
} |
| 721 |
- |
|
| 722 |
-fn team_refresh_button(cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 723 |
- div() |
|
| 724 |
- .id("refresh-team-details")
|
|
| 725 |
- .flex_none() |
|
| 726 |
- .h_8() |
|
| 727 |
- .px_3() |
|
| 728 |
- .rounded_md() |
|
| 729 |
- .bg(rgb(0xebf1f0)) |
|
| 730 |
- .text_color(rgb(0x53666d)) |
|
| 731 |
- .text_xs() |
|
| 732 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 733 |
- .flex() |
|
| 734 |
- .items_center() |
|
| 735 |
- .justify_center() |
|
| 736 |
- .gap_2() |
|
| 737 |
- .cursor_pointer() |
|
| 738 |
- .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 739 |
- .on_click(cx.listener(SettingsWindow::refresh_team_details)) |
|
| 740 |
- .child(lucide_icon("icons/refresh-cw.svg"))
|
|
| 741 |
- .child("Refresh")
|
|
| 1147 |
+ } else {
|
|
| 1148 |
+ cx.notify(); |
|
| 742 | 1149 |
} |
| 743 |
- |
|
| 744 |
-fn settings_label(label: &'static str) -> gpui::Div {
|
|
| 745 |
- div() |
|
| 746 |
- .text_xs() |
|
| 747 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 748 |
- .text_color(rgb(0x66767c)) |
|
| 749 |
- .child(label) |
|
| 750 | 1150 |
} |
| 751 | 1151 |
|
| 752 |
-fn developer_settings_section( |
|
| 753 |
- title: &'static str, |
|
| 754 |
- detail: &'static str, |
|
| 755 |
- body: impl IntoElement, |
|
| 756 |
-) -> gpui::Div {
|
|
| 757 |
- div() |
|
| 758 |
- .p_3() |
|
| 759 |
- .rounded_md() |
|
| 760 |
- .border_1() |
|
| 761 |
- .border_color(rgb(0xd8e0df)) |
|
| 762 |
- .bg(rgb(0xffffff)) |
|
| 763 |
- .flex() |
|
| 764 |
- .flex_col() |
|
| 765 |
- .gap_3() |
|
| 766 |
- .child( |
|
| 767 |
- div() |
|
| 768 |
- .flex() |
|
| 769 |
- .flex_col() |
|
| 770 |
- .gap_0p5() |
|
| 771 |
- .child( |
|
| 772 |
- div() |
|
| 773 |
- .text_sm() |
|
| 774 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 775 |
- .text_color(rgb(0x24333a)) |
|
| 776 |
- .child(title), |
|
| 777 |
- ) |
|
| 778 |
- .child(div().text_xs().text_color(rgb(0x6a7a81)).child(detail)), |
|
| 779 |
- ) |
|
| 780 |
- .child(body) |
|
| 781 |
-} |
|
| 1152 |
+ fn edit_app_icon(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 1153 |
+ window.focus(&self.focus_handle, cx); |
|
| 1154 |
+ let receiver = cx.prompt_for_paths(PathPromptOptions {
|
|
| 1155 |
+ files: true, |
|
| 1156 |
+ directories: false, |
|
| 1157 |
+ multiple: false, |
|
| 1158 |
+ prompt: Some("Select App Icon".into()),
|
|
| 1159 |
+ }); |
|
| 782 | 1160 |
|
| 783 |
-fn settings_team_row(team: &TeamOption, selected: bool) -> gpui::Div {
|
|
| 784 |
- div() |
|
| 785 |
- .h(px(COMBO_ITEM_HEIGHT)) |
|
| 786 |
- .px_3() |
|
| 787 |
- .rounded_md() |
|
| 788 |
- .border_1() |
|
| 789 |
- .border_color(if selected {
|
|
| 790 |
- rgb(0x0f6f7a) |
|
| 791 |
- } else {
|
|
| 792 |
- rgb(0xd8e0df) |
|
| 793 |
- }) |
|
| 794 |
- .bg(if selected {
|
|
| 795 |
- rgb(0xf0fbfa) |
|
| 796 |
- } else {
|
|
| 797 |
- rgb(0xffffff) |
|
| 798 |
- }) |
|
| 799 |
- .cursor_pointer() |
|
| 800 |
- .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 801 |
- .flex() |
|
| 802 |
- .items_center() |
|
| 803 |
- .child(combo_item_content( |
|
| 804 |
- team.identifier.clone(), |
|
| 805 |
- team.name.clone(), |
|
| 806 |
- developer_account_title(&team.role), |
|
| 807 |
- )) |
|
| 1161 |
+ cx.spawn(async move |settings, cx| {
|
|
| 1162 |
+ let paths = match receiver.await {
|
|
| 1163 |
+ Ok(Ok(Some(paths))) => paths, |
|
| 1164 |
+ Ok(Ok(None)) | Err(_) => return, |
|
| 1165 |
+ Ok(Err(error)) => {
|
|
| 1166 |
+ eprintln!("Failed to select app icon: {error}");
|
|
| 1167 |
+ return; |
|
| 808 | 1168 |
} |
| 1169 |
+ }; |
|
| 809 | 1170 |
|
| 810 |
-fn app_id_section( |
|
| 811 |
- team: &TeamOption, |
|
| 812 |
- auto_app_id: bool, |
|
| 813 |
- selected_app_id: usize, |
|
| 814 |
- picker_open: bool, |
|
| 815 |
- cx: &mut Context<SettingsWindow>, |
|
| 816 |
-) -> gpui::Div {
|
|
| 817 |
- let manual_enabled = !auto_app_id; |
|
| 818 |
- let selected_app_id = selected_app_id.min(team.app_ids.len().saturating_sub(1)); |
|
| 819 |
- let selected = team.app_ids.get(selected_app_id); |
|
| 1171 |
+ let Some(path) = paths.into_iter().next() else {
|
|
| 1172 |
+ return; |
|
| 1173 |
+ }; |
|
| 820 | 1174 |
|
| 821 |
- developer_settings_section( |
|
| 822 |
- "App ID", |
|
| 823 |
- "Identifier used when creating or selecting the provisioning profile.", |
|
| 824 |
- div() |
|
| 825 |
- .flex() |
|
| 826 |
- .flex_col() |
|
| 827 |
- .gap_3() |
|
| 828 |
- .child(auto_app_id_checkbox(auto_app_id, cx)) |
|
| 829 |
- .child( |
|
| 830 |
- div() |
|
| 831 |
- .flex() |
|
| 832 |
- .flex_col() |
|
| 833 |
- .gap_2() |
|
| 834 |
- .child(settings_label("Manual App ID"))
|
|
| 835 |
- .child( |
|
| 836 |
- div() |
|
| 837 |
- .flex() |
|
| 838 |
- .items_center() |
|
| 839 |
- .gap_2() |
|
| 840 |
- .when_some(selected, |this, selected| {
|
|
| 841 |
- this.child( |
|
| 842 |
- div() |
|
| 843 |
- .min_w_0() |
|
| 844 |
- .flex_1() |
|
| 845 |
- .when(!manual_enabled, |this| this.opacity(0.5)) |
|
| 846 |
- .child(combo_with_popover( |
|
| 847 |
- "app-id-popover-scroll", |
|
| 848 |
- combo_button( |
|
| 849 |
- selected.identifier.clone(), |
|
| 850 |
- selected.name.clone(), |
|
| 851 |
- selected.kind.clone(), |
|
| 852 |
- picker_open && manual_enabled, |
|
| 853 |
- ) |
|
| 854 |
- .id("app-id-combobox")
|
|
| 855 |
- .when( |
|
| 856 |
- manual_enabled, |
|
| 857 |
- |this| {
|
|
| 858 |
- this.on_click(cx.listener( |
|
| 859 |
- SettingsWindow::toggle_app_id_picker, |
|
| 860 |
- )) |
|
| 861 |
- }, |
|
| 862 |
- ), |
|
| 863 |
- picker_open && manual_enabled, |
|
| 864 |
- dropdown_list((0..team.app_ids.len()).map(|index| {
|
|
| 865 |
- settings_app_id_row( |
|
| 866 |
- &team.app_ids[index], |
|
| 867 |
- index == selected_app_id, |
|
| 868 |
- ) |
|
| 869 |
- .id(("app-id-row", index))
|
|
| 870 |
- .on_click(cx.listener( |
|
| 871 |
- move |this, event, window, cx| {
|
|
| 872 |
- this.select_app_id(index, event, window, cx) |
|
| 873 |
- }, |
|
| 874 |
- )) |
|
| 875 |
- })) |
|
| 876 |
- .id("app-id-options"),
|
|
| 877 |
- )), |
|
| 878 |
- ) |
|
| 1175 |
+ let _ = settings.update(cx, |settings, cx| {
|
|
| 1176 |
+ if let SettingsWindowRequest::AppSettings {
|
|
| 1177 |
+ parent, |
|
| 1178 |
+ app_index, |
|
| 1179 |
+ app, |
|
| 1180 |
+ .. |
|
| 1181 |
+ } = &mut settings.request |
|
| 1182 |
+ {
|
|
| 1183 |
+ app.icon_override_path = Some(path.to_string_lossy().to_string().into()); |
|
| 1184 |
+ let app_index = *app_index; |
|
| 1185 |
+ let updated = app.clone(); |
|
| 1186 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 1187 |
+ view.replace_app_from_settings(app_index, updated, cx); |
|
| 1188 |
+ }); |
|
| 1189 |
+ } |
|
| 1190 |
+ cx.notify(); |
|
| 1191 |
+ }); |
|
| 879 | 1192 |
}) |
| 880 |
- .child(app_id_actions(!manual_enabled, cx)), |
|
| 881 |
- ), |
|
| 882 |
- ), |
|
| 883 |
- ) |
|
| 1193 |
+ .detach(); |
|
| 884 | 1194 |
} |
| 885 | 1195 |
|
| 886 |
-fn auto_app_id_checkbox(checked: bool, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 887 |
- div() |
|
| 888 |
- .id("auto-app-id")
|
|
| 889 |
- .p_2() |
|
| 890 |
- .rounded_md() |
|
| 891 |
- .bg(rgb(0xf6f9f9)) |
|
| 892 |
- .flex() |
|
| 893 |
- .items_center() |
|
| 894 |
- .gap_2() |
|
| 895 |
- .cursor_pointer() |
|
| 896 |
- .hover(|style| style.bg(rgb(0xebf1f0))) |
|
| 897 |
- .on_click(cx.listener(SettingsWindow::toggle_auto_app_id)) |
|
| 898 |
- .child( |
|
| 899 |
- div() |
|
| 900 |
- .flex_none() |
|
| 901 |
- .w_4() |
|
| 902 |
- .h_4() |
|
| 903 |
- .rounded_sm() |
|
| 904 |
- .border_1() |
|
| 905 |
- .border_color(if checked {
|
|
| 906 |
- rgb(0x173f45) |
|
| 907 |
- } else {
|
|
| 908 |
- rgb(0xaebcba) |
|
| 909 |
- }) |
|
| 910 |
- .bg(if checked {
|
|
| 911 |
- rgb(0x173f45) |
|
| 1196 |
+ fn begin_app_detail_edit( |
|
| 1197 |
+ &mut self, |
|
| 1198 |
+ field: AppMetadataField, |
|
| 1199 |
+ _: &ClickEvent, |
|
| 1200 |
+ window: &mut Window, |
|
| 1201 |
+ cx: &mut Context<Self>, |
|
| 1202 |
+ ) {
|
|
| 1203 |
+ window.focus(&self.focus_handle, cx); |
|
| 1204 |
+ let SettingsWindowRequest::AppSettings { app, .. } = &self.request else {
|
|
| 1205 |
+ return; |
|
| 1206 |
+ }; |
|
| 1207 |
+ if field == AppMetadataField::SupportedDevices {
|
|
| 1208 |
+ self.app_detail_edit = Some(AppDetailEdit::SupportedDevices {
|
|
| 1209 |
+ selected: app.supported_devices().to_vec(), |
|
| 1210 |
+ }); |
|
| 912 | 1211 |
} else {
|
| 913 |
- rgb(0xffffff) |
|
| 914 |
- }) |
|
| 915 |
- .flex() |
|
| 916 |
- .items_center() |
|
| 917 |
- .justify_center() |
|
| 918 |
- .when(checked, |this| {
|
|
| 919 |
- this.child(div().w_2().h_2().rounded_sm().bg(rgb(0xffffff))) |
|
| 920 |
- }), |
|
| 921 |
- ) |
|
| 922 |
- .child( |
|
| 923 |
- div() |
|
| 924 |
- .min_w_0() |
|
| 925 |
- .flex() |
|
| 926 |
- .flex_1() |
|
| 927 |
- .flex_col() |
|
| 928 |
- .gap_0p5() |
|
| 929 |
- .child( |
|
| 930 |
- div() |
|
| 931 |
- .text_xs() |
|
| 932 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 933 |
- .text_color(rgb(0x24333a)) |
|
| 934 |
- .child("Automatically pick or create an App ID"),
|
|
| 935 |
- ) |
|
| 936 |
- .child( |
|
| 937 |
- div() |
|
| 938 |
- .text_xs() |
|
| 939 |
- .text_color(rgb(0x6a7a81)) |
|
| 940 |
- .child("Manual App ID selection stays visible below."),
|
|
| 941 |
- ), |
|
| 942 |
- ) |
|
| 1212 |
+ let input = cx.new(|cx| EditLine::new(app.field(field).value().clone(), cx)); |
|
| 1213 |
+ self.app_detail_edit = Some(AppDetailEdit::Text {
|
|
| 1214 |
+ field, |
|
| 1215 |
+ input: input.clone(), |
|
| 1216 |
+ }); |
|
| 1217 |
+ input.update(cx, |input, cx| input.focus(window, cx)); |
|
| 1218 |
+ } |
|
| 1219 |
+ cx.notify(); |
|
| 943 | 1220 |
} |
| 944 | 1221 |
|
| 945 |
-fn app_id_actions(disabled: bool, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 946 |
- div() |
|
| 947 |
- .flex_none() |
|
| 948 |
- .flex() |
|
| 949 |
- .items_center() |
|
| 950 |
- .gap_1() |
|
| 951 |
- .child(app_id_icon_button( |
|
| 952 |
- "add-app-id", |
|
| 953 |
- "icons/plus.svg", |
|
| 954 |
- disabled, |
|
| 955 |
- cx.listener(SettingsWindow::add_app_id), |
|
| 956 |
- )) |
|
| 957 |
- .child(app_id_icon_button( |
|
| 958 |
- "remove-app-id", |
|
| 959 |
- "icons/minus.svg", |
|
| 960 |
- disabled, |
|
| 961 |
- cx.listener(SettingsWindow::remove_app_id), |
|
| 962 |
- )) |
|
| 963 |
- .child(app_id_icon_button( |
|
| 964 |
- "edit-app-id", |
|
| 965 |
- "icons/pencil.svg", |
|
| 966 |
- disabled, |
|
| 967 |
- cx.listener(SettingsWindow::edit_app_id), |
|
| 968 |
- )) |
|
| 969 |
-} |
|
| 970 |
- |
|
| 971 |
-fn app_id_icon_button( |
|
| 972 |
- id: &'static str, |
|
| 973 |
- icon: &'static str, |
|
| 974 |
- disabled: bool, |
|
| 975 |
- listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, |
|
| 976 |
-) -> impl IntoElement {
|
|
| 977 |
- div() |
|
| 978 |
- .flex_none() |
|
| 979 |
- .w_6() |
|
| 980 |
- .h_6() |
|
| 981 |
- .rounded_md() |
|
| 982 |
- .bg(rgb(0xebf1f0)) |
|
| 983 |
- .text_color(rgb(0x53666d)) |
|
| 984 |
- .flex() |
|
| 985 |
- .items_center() |
|
| 986 |
- .justify_center() |
|
| 987 |
- .id(id) |
|
| 988 |
- .when(disabled, |this| this.opacity(0.45)) |
|
| 989 |
- .when(!disabled, |this| {
|
|
| 990 |
- this.cursor_pointer() |
|
| 991 |
- .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 992 |
- .on_click(listener) |
|
| 993 |
- }) |
|
| 994 |
- .child(lucide_icon(icon)) |
|
| 1222 |
+ fn toggle_supported_device_family( |
|
| 1223 |
+ &mut self, |
|
| 1224 |
+ family: SupportedDeviceFamily, |
|
| 1225 |
+ _: &ClickEvent, |
|
| 1226 |
+ window: &mut Window, |
|
| 1227 |
+ cx: &mut Context<Self>, |
|
| 1228 |
+ ) {
|
|
| 1229 |
+ window.focus(&self.focus_handle, cx); |
|
| 1230 |
+ let Some(AppDetailEdit::SupportedDevices { selected }) = &mut self.app_detail_edit else {
|
|
| 1231 |
+ return; |
|
| 1232 |
+ }; |
|
| 1233 |
+ |
|
| 1234 |
+ if let Some(index) = selected.iter().position(|candidate| *candidate == family) {
|
|
| 1235 |
+ selected.remove(index); |
|
| 1236 |
+ } else {
|
|
| 1237 |
+ selected.push(family); |
|
| 1238 |
+ selected.sort(); |
|
| 1239 |
+ selected.dedup(); |
|
| 1240 |
+ } |
|
| 1241 |
+ cx.notify(); |
|
| 1242 |
+ } |
|
| 1243 |
+ |
|
| 1244 |
+ fn save_app_detail_edit_from_button( |
|
| 1245 |
+ &mut self, |
|
| 1246 |
+ _: &ClickEvent, |
|
| 1247 |
+ window: &mut Window, |
|
| 1248 |
+ cx: &mut Context<Self>, |
|
| 1249 |
+ ) {
|
|
| 1250 |
+ window.focus(&self.focus_handle, cx); |
|
| 1251 |
+ self.save_app_detail_edit(cx); |
|
| 1252 |
+ } |
|
| 1253 |
+ |
|
| 1254 |
+ fn cancel_app_detail_edit( |
|
| 1255 |
+ &mut self, |
|
| 1256 |
+ _: &ClickEvent, |
|
| 1257 |
+ window: &mut Window, |
|
| 1258 |
+ cx: &mut Context<Self>, |
|
| 1259 |
+ ) {
|
|
| 1260 |
+ window.focus(&self.focus_handle, cx); |
|
| 1261 |
+ self.app_detail_edit = None; |
|
| 1262 |
+ cx.notify(); |
|
| 1263 |
+ } |
|
| 1264 |
+ |
|
| 1265 |
+ fn handle_app_detail_editor_key( |
|
| 1266 |
+ &mut self, |
|
| 1267 |
+ event: &KeyDownEvent, |
|
| 1268 |
+ window: &mut Window, |
|
| 1269 |
+ cx: &mut Context<Self>, |
|
| 1270 |
+ ) {
|
|
| 1271 |
+ if self.app_detail_edit.is_none() {
|
|
| 1272 |
+ return; |
|
| 1273 |
+ } |
|
| 1274 |
+ |
|
| 1275 |
+ match event.keystroke.key.as_str() {
|
|
| 1276 |
+ "enter" => {
|
|
| 1277 |
+ cx.stop_propagation(); |
|
| 1278 |
+ window.prevent_default(); |
|
| 1279 |
+ self.save_app_detail_edit(cx); |
|
| 1280 |
+ } |
|
| 1281 |
+ "escape" => {
|
|
| 1282 |
+ cx.stop_propagation(); |
|
| 1283 |
+ window.prevent_default(); |
|
| 1284 |
+ self.app_detail_edit = None; |
|
| 1285 |
+ cx.notify(); |
|
| 1286 |
+ } |
|
| 1287 |
+ _ => {}
|
|
| 1288 |
+ } |
|
| 1289 |
+ } |
|
| 1290 |
+ |
|
| 1291 |
+ fn handle_app_settings_key( |
|
| 1292 |
+ &mut self, |
|
| 1293 |
+ event: &KeyDownEvent, |
|
| 1294 |
+ window: &mut Window, |
|
| 1295 |
+ cx: &mut Context<Self>, |
|
| 1296 |
+ ) {
|
|
| 1297 |
+ if self.entitlement_edit.is_some() {
|
|
| 1298 |
+ self.handle_entitlement_editor_key(event, window, cx); |
|
| 1299 |
+ } else {
|
|
| 1300 |
+ self.handle_app_detail_editor_key(event, window, cx); |
|
| 1301 |
+ } |
|
| 1302 |
+ } |
|
| 1303 |
+ |
|
| 1304 |
+ fn save_app_detail_edit(&mut self, cx: &mut Context<Self>) {
|
|
| 1305 |
+ let Some(edit) = self.app_detail_edit.take() else {
|
|
| 1306 |
+ return; |
|
| 1307 |
+ }; |
|
| 1308 |
+ |
|
| 1309 |
+ let SettingsWindowRequest::AppSettings {
|
|
| 1310 |
+ parent, |
|
| 1311 |
+ app_index, |
|
| 1312 |
+ app, |
|
| 1313 |
+ .. |
|
| 1314 |
+ } = &mut self.request |
|
| 1315 |
+ else {
|
|
| 1316 |
+ cx.notify(); |
|
| 1317 |
+ return; |
|
| 1318 |
+ }; |
|
| 1319 |
+ |
|
| 1320 |
+ match edit {
|
|
| 1321 |
+ AppDetailEdit::Text { field, input } => {
|
|
| 1322 |
+ let value = input.read(cx).value().to_string(); |
|
| 1323 |
+ app.set_field_override(field, value); |
|
| 1324 |
+ } |
|
| 1325 |
+ AppDetailEdit::SupportedDevices { selected } => {
|
|
| 1326 |
+ app.set_supported_devices_override(selected); |
|
| 1327 |
+ } |
|
| 1328 |
+ } |
|
| 1329 |
+ let app_index = *app_index; |
|
| 1330 |
+ let updated = app.clone(); |
|
| 1331 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 1332 |
+ view.replace_app_from_settings(app_index, updated, cx); |
|
| 1333 |
+ }); |
|
| 1334 |
+ cx.notify(); |
|
| 1335 |
+ } |
|
| 1336 |
+ |
|
| 1337 |
+ fn revert_app_detail_field( |
|
| 1338 |
+ &mut self, |
|
| 1339 |
+ field: AppMetadataField, |
|
| 1340 |
+ _: &ClickEvent, |
|
| 1341 |
+ window: &mut Window, |
|
| 1342 |
+ cx: &mut Context<Self>, |
|
| 1343 |
+ ) {
|
|
| 1344 |
+ window.focus(&self.focus_handle, cx); |
|
| 1345 |
+ let SettingsWindowRequest::AppSettings {
|
|
| 1346 |
+ parent, |
|
| 1347 |
+ app_index, |
|
| 1348 |
+ app, |
|
| 1349 |
+ .. |
|
| 1350 |
+ } = &mut self.request |
|
| 1351 |
+ else {
|
|
| 1352 |
+ return; |
|
| 1353 |
+ }; |
|
| 1354 |
+ |
|
| 1355 |
+ app.clear_field_override(field); |
|
| 1356 |
+ if self |
|
| 1357 |
+ .app_detail_edit |
|
| 1358 |
+ .as_ref() |
|
| 1359 |
+ .is_some_and(|edit| edit.field() == field) |
|
| 1360 |
+ {
|
|
| 1361 |
+ self.app_detail_edit = None; |
|
| 1362 |
+ } |
|
| 1363 |
+ let app_index = *app_index; |
|
| 1364 |
+ let updated = app.clone(); |
|
| 1365 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 1366 |
+ view.replace_app_from_settings(app_index, updated, cx); |
|
| 1367 |
+ }); |
|
| 1368 |
+ cx.notify(); |
|
| 1369 |
+ } |
|
| 1370 |
+ |
|
| 1371 |
+ fn add_entitlement(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 1372 |
+ window.focus(&self.focus_handle, cx); |
|
| 1373 |
+ let SettingsWindowRequest::AppSettings {
|
|
| 1374 |
+ parent, |
|
| 1375 |
+ app_index, |
|
| 1376 |
+ app, |
|
| 1377 |
+ team_id, |
|
| 1378 |
+ .. |
|
| 1379 |
+ } = &mut self.request |
|
| 1380 |
+ else {
|
|
| 1381 |
+ return; |
|
| 1382 |
+ }; |
|
| 1383 |
+ |
|
| 1384 |
+ let mut entitlements = app.effective_entitlements(team_id); |
|
| 1385 |
+ entitlements.push(AppEntitlement {
|
|
| 1386 |
+ key: "new.entitlement".into(), |
|
| 1387 |
+ value: EntitlementValue::String("".into()),
|
|
| 1388 |
+ }); |
|
| 1389 |
+ let selected = entitlements.len().saturating_sub(1); |
|
| 1390 |
+ app.entitlement_overrides = Some(entitlements); |
|
| 1391 |
+ let app_index = *app_index; |
|
| 1392 |
+ let updated = app.clone(); |
|
| 1393 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 1394 |
+ view.replace_app_from_settings(app_index, updated, cx); |
|
| 1395 |
+ }); |
|
| 1396 |
+ let input = cx.new(|cx| EditLine::new("new.entitlement", cx));
|
|
| 1397 |
+ self.selected_entitlement = Some(selected); |
|
| 1398 |
+ self.entitlement_type_picker_open = false; |
|
| 1399 |
+ self.entitlement_edit = Some(EntitlementEdit::Text {
|
|
| 1400 |
+ row: selected, |
|
| 1401 |
+ field: EntitlementField::Key, |
|
| 1402 |
+ input: input.clone(), |
|
| 1403 |
+ }); |
|
| 1404 |
+ input.update(cx, |input, cx| input.focus(window, cx)); |
|
| 1405 |
+ cx.notify(); |
|
| 1406 |
+ } |
|
| 1407 |
+ |
|
| 1408 |
+ fn remove_entitlement(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 1409 |
+ window.focus(&self.focus_handle, cx); |
|
| 1410 |
+ let SettingsWindowRequest::AppSettings {
|
|
| 1411 |
+ parent, |
|
| 1412 |
+ app_index, |
|
| 1413 |
+ app, |
|
| 1414 |
+ team_id, |
|
| 1415 |
+ .. |
|
| 1416 |
+ } = &mut self.request |
|
| 1417 |
+ else {
|
|
| 1418 |
+ return; |
|
| 1419 |
+ }; |
|
| 1420 |
+ |
|
| 1421 |
+ let mut entitlements = app.effective_entitlements(team_id); |
|
| 1422 |
+ if entitlements.is_empty() {
|
|
| 1423 |
+ return; |
|
| 1424 |
+ } |
|
| 1425 |
+ let index = self |
|
| 1426 |
+ .selected_entitlement |
|
| 1427 |
+ .filter(|index| *index < entitlements.len()) |
|
| 1428 |
+ .unwrap_or_else(|| entitlements.len() - 1); |
|
| 1429 |
+ entitlements.remove(index); |
|
| 1430 |
+ let entitlements_len = entitlements.len(); |
|
| 1431 |
+ app.entitlement_overrides = Some(entitlements); |
|
| 1432 |
+ self.selected_entitlement = if entitlements_len == 0 {
|
|
| 1433 |
+ None |
|
| 1434 |
+ } else {
|
|
| 1435 |
+ Some(index.min(entitlements_len - 1)) |
|
| 1436 |
+ }; |
|
| 1437 |
+ self.entitlement_edit = None; |
|
| 1438 |
+ self.entitlement_type_picker_open = false; |
|
| 1439 |
+ let app_index = *app_index; |
|
| 1440 |
+ let updated = app.clone(); |
|
| 1441 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 1442 |
+ view.replace_app_from_settings(app_index, updated, cx); |
|
| 1443 |
+ }); |
|
| 1444 |
+ cx.notify(); |
|
| 1445 |
+ } |
|
| 1446 |
+ |
|
| 1447 |
+ fn revert_entitlements(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
| 1448 |
+ window.focus(&self.focus_handle, cx); |
|
| 1449 |
+ let SettingsWindowRequest::AppSettings {
|
|
| 1450 |
+ parent, |
|
| 1451 |
+ app_index, |
|
| 1452 |
+ app, |
|
| 1453 |
+ .. |
|
| 1454 |
+ } = &mut self.request |
|
| 1455 |
+ else {
|
|
| 1456 |
+ return; |
|
| 1457 |
+ }; |
|
| 1458 |
+ |
|
| 1459 |
+ app.entitlement_overrides = None; |
|
| 1460 |
+ self.selected_entitlement = None; |
|
| 1461 |
+ self.entitlement_edit = None; |
|
| 1462 |
+ self.entitlement_type_picker_open = false; |
|
| 1463 |
+ let app_index = *app_index; |
|
| 1464 |
+ let updated = app.clone(); |
|
| 1465 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 1466 |
+ view.replace_app_from_settings(app_index, updated, cx); |
|
| 1467 |
+ }); |
|
| 1468 |
+ cx.notify(); |
|
| 1469 |
+ } |
|
| 1470 |
+ |
|
| 1471 |
+ fn revert_entitlement( |
|
| 1472 |
+ &mut self, |
|
| 1473 |
+ row: usize, |
|
| 1474 |
+ _: &ClickEvent, |
|
| 1475 |
+ window: &mut Window, |
|
| 1476 |
+ cx: &mut Context<Self>, |
|
| 1477 |
+ ) {
|
|
| 1478 |
+ cx.stop_propagation(); |
|
| 1479 |
+ window.focus(&self.focus_handle, cx); |
|
| 1480 |
+ let SettingsWindowRequest::AppSettings {
|
|
| 1481 |
+ parent, |
|
| 1482 |
+ app_index, |
|
| 1483 |
+ app, |
|
| 1484 |
+ team_id, |
|
| 1485 |
+ .. |
|
| 1486 |
+ } = &mut self.request |
|
| 1487 |
+ else {
|
|
| 1488 |
+ return; |
|
| 1489 |
+ }; |
|
| 1490 |
+ |
|
| 1491 |
+ let mut entitlements = app.effective_entitlements(team_id); |
|
| 1492 |
+ if row >= entitlements.len() {
|
|
| 1493 |
+ return; |
|
| 1494 |
+ } |
|
| 1495 |
+ |
|
| 1496 |
+ if let Some(default) = entitlement_revert_target( |
|
| 1497 |
+ &entitlements[row], |
|
| 1498 |
+ row, |
|
| 1499 |
+ &app.default_effective_entitlements(team_id), |
|
| 1500 |
+ ) {
|
|
| 1501 |
+ entitlements[row] = default; |
|
| 1502 |
+ } else {
|
|
| 1503 |
+ entitlements.remove(row); |
|
| 1504 |
+ } |
|
| 1505 |
+ |
|
| 1506 |
+ let defaults = app.default_effective_entitlements(team_id); |
|
| 1507 |
+ app.entitlement_overrides = (entitlements != defaults).then_some(entitlements); |
|
| 1508 |
+ self.selected_entitlement = app.entitlement_overrides.as_ref().and_then(|entitlements| {
|
|
| 1509 |
+ (!entitlements.is_empty()).then_some(row.min(entitlements.len() - 1)) |
|
| 1510 |
+ }); |
|
| 1511 |
+ self.entitlement_edit = None; |
|
| 1512 |
+ self.entitlement_type_picker_open = false; |
|
| 1513 |
+ let app_index = *app_index; |
|
| 1514 |
+ let updated = app.clone(); |
|
| 1515 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 1516 |
+ view.replace_app_from_settings(app_index, updated, cx); |
|
| 1517 |
+ }); |
|
| 1518 |
+ cx.notify(); |
|
| 1519 |
+ } |
|
| 1520 |
+ |
|
| 1521 |
+ fn begin_entitlement_edit( |
|
| 1522 |
+ &mut self, |
|
| 1523 |
+ row: usize, |
|
| 1524 |
+ field: EntitlementField, |
|
| 1525 |
+ _: &ClickEvent, |
|
| 1526 |
+ window: &mut Window, |
|
| 1527 |
+ cx: &mut Context<Self>, |
|
| 1528 |
+ ) {
|
|
| 1529 |
+ window.focus(&self.focus_handle, cx); |
|
| 1530 |
+ let SettingsWindowRequest::AppSettings { app, team_id, .. } = &self.request else {
|
|
| 1531 |
+ return; |
|
| 1532 |
+ }; |
|
| 1533 |
+ let entitlements = app.effective_entitlements(team_id); |
|
| 1534 |
+ let Some(entitlement) = entitlements.get(row) else {
|
|
| 1535 |
+ return; |
|
| 1536 |
+ }; |
|
| 1537 |
+ self.selected_entitlement = Some(row); |
|
| 1538 |
+ self.entitlement_type_picker_open = false; |
|
| 1539 |
+ if field == EntitlementField::ValueType {
|
|
| 1540 |
+ self.entitlement_type_picker_open = true; |
|
| 1541 |
+ self.entitlement_edit = Some(EntitlementEdit::Type {
|
|
| 1542 |
+ row, |
|
| 1543 |
+ selected: entitlement.value.type_label().into(), |
|
| 1544 |
+ }); |
|
| 1545 |
+ cx.notify(); |
|
| 1546 |
+ return; |
|
| 1547 |
+ } |
|
| 1548 |
+ |
|
| 1549 |
+ if field == EntitlementField::Value {
|
|
| 1550 |
+ match &entitlement.value {
|
|
| 1551 |
+ EntitlementValue::Boolean(value) => {
|
|
| 1552 |
+ self.entitlement_edit = Some(EntitlementEdit::Boolean { row, value: *value });
|
|
| 1553 |
+ cx.notify(); |
|
| 1554 |
+ return; |
|
| 1555 |
+ } |
|
| 1556 |
+ EntitlementValue::Array(_) => {
|
|
| 1557 |
+ let items = entitlement |
|
| 1558 |
+ .value |
|
| 1559 |
+ .array_edit_values() |
|
| 1560 |
+ .into_iter() |
|
| 1561 |
+ .map(|value| cx.new(|cx| EditLine::new(value, cx))) |
|
| 1562 |
+ .collect::<Vec<_>>(); |
|
| 1563 |
+ self.entitlement_edit = Some(EntitlementEdit::Array {
|
|
| 1564 |
+ row, |
|
| 1565 |
+ items: items.clone(), |
|
| 1566 |
+ }); |
|
| 1567 |
+ if let Some(input) = items.first() {
|
|
| 1568 |
+ input.update(cx, |input, cx| input.focus(window, cx)); |
|
| 1569 |
+ } |
|
| 1570 |
+ cx.notify(); |
|
| 1571 |
+ return; |
|
| 1572 |
+ } |
|
| 1573 |
+ _ => {}
|
|
| 1574 |
+ } |
|
| 1575 |
+ } |
|
| 1576 |
+ |
|
| 1577 |
+ let value = entitlement_field_value(entitlement, field).clone(); |
|
| 1578 |
+ let input = cx.new(|cx| EditLine::new(value, cx)); |
|
| 1579 |
+ self.entitlement_edit = Some(EntitlementEdit::Text {
|
|
| 1580 |
+ row, |
|
| 1581 |
+ field, |
|
| 1582 |
+ input: input.clone(), |
|
| 1583 |
+ }); |
|
| 1584 |
+ input.update(cx, |input, cx| input.focus(window, cx)); |
|
| 1585 |
+ cx.notify(); |
|
| 1586 |
+ } |
|
| 1587 |
+ |
|
| 1588 |
+ fn toggle_boolean_entitlement_value( |
|
| 1589 |
+ &mut self, |
|
| 1590 |
+ _: &ClickEvent, |
|
| 1591 |
+ window: &mut Window, |
|
| 1592 |
+ cx: &mut Context<Self>, |
|
| 1593 |
+ ) {
|
|
| 1594 |
+ cx.stop_propagation(); |
|
| 1595 |
+ window.focus(&self.focus_handle, cx); |
|
| 1596 |
+ let Some(EntitlementEdit::Boolean { value: edited, .. }) = &mut self.entitlement_edit
|
|
| 1597 |
+ else {
|
|
| 1598 |
+ return; |
|
| 1599 |
+ }; |
|
| 1600 |
+ *edited = !*edited; |
|
| 1601 |
+ cx.notify(); |
|
| 1602 |
+ } |
|
| 1603 |
+ |
|
| 1604 |
+ fn add_entitlement_array_item( |
|
| 1605 |
+ &mut self, |
|
| 1606 |
+ _: &ClickEvent, |
|
| 1607 |
+ window: &mut Window, |
|
| 1608 |
+ cx: &mut Context<Self>, |
|
| 1609 |
+ ) {
|
|
| 1610 |
+ cx.stop_propagation(); |
|
| 1611 |
+ window.focus(&self.focus_handle, cx); |
|
| 1612 |
+ let Some(EntitlementEdit::Array { items, .. }) = &mut self.entitlement_edit else {
|
|
| 1613 |
+ return; |
|
| 1614 |
+ }; |
|
| 1615 |
+ let input = cx.new(|cx| EditLine::new("", cx));
|
|
| 1616 |
+ items.push(input.clone()); |
|
| 1617 |
+ input.update(cx, |input, cx| input.focus(window, cx)); |
|
| 1618 |
+ cx.notify(); |
|
| 1619 |
+ } |
|
| 1620 |
+ |
|
| 1621 |
+ fn remove_entitlement_array_item( |
|
| 1622 |
+ &mut self, |
|
| 1623 |
+ index: usize, |
|
| 1624 |
+ _: &ClickEvent, |
|
| 1625 |
+ window: &mut Window, |
|
| 1626 |
+ cx: &mut Context<Self>, |
|
| 1627 |
+ ) {
|
|
| 1628 |
+ cx.stop_propagation(); |
|
| 1629 |
+ window.focus(&self.focus_handle, cx); |
|
| 1630 |
+ let Some(EntitlementEdit::Array { items, .. }) = &mut self.entitlement_edit else {
|
|
| 1631 |
+ return; |
|
| 1632 |
+ }; |
|
| 1633 |
+ if index < items.len() {
|
|
| 1634 |
+ items.remove(index); |
|
| 1635 |
+ } |
|
| 1636 |
+ cx.notify(); |
|
| 1637 |
+ } |
|
| 1638 |
+ |
|
| 1639 |
+ fn toggle_entitlement_type_picker( |
|
| 1640 |
+ &mut self, |
|
| 1641 |
+ _: &ClickEvent, |
|
| 1642 |
+ window: &mut Window, |
|
| 1643 |
+ cx: &mut Context<Self>, |
|
| 1644 |
+ ) {
|
|
| 1645 |
+ cx.stop_propagation(); |
|
| 1646 |
+ window.focus(&self.focus_handle, cx); |
|
| 1647 |
+ if matches!(self.entitlement_edit, Some(EntitlementEdit::Type { .. })) {
|
|
| 1648 |
+ self.entitlement_type_picker_open = !self.entitlement_type_picker_open; |
|
| 1649 |
+ } |
|
| 1650 |
+ cx.notify(); |
|
| 1651 |
+ } |
|
| 1652 |
+ |
|
| 1653 |
+ fn select_entitlement_type( |
|
| 1654 |
+ &mut self, |
|
| 1655 |
+ label: &'static str, |
|
| 1656 |
+ _: &ClickEvent, |
|
| 1657 |
+ window: &mut Window, |
|
| 1658 |
+ cx: &mut Context<Self>, |
|
| 1659 |
+ ) {
|
|
| 1660 |
+ cx.stop_propagation(); |
|
| 1661 |
+ window.focus(&self.focus_handle, cx); |
|
| 1662 |
+ let Some(EntitlementEdit::Type { selected, .. }) = &mut self.entitlement_edit else {
|
|
| 1663 |
+ return; |
|
| 1664 |
+ }; |
|
| 1665 |
+ *selected = label.into(); |
|
| 1666 |
+ self.entitlement_type_picker_open = false; |
|
| 1667 |
+ cx.notify(); |
|
| 1668 |
+ } |
|
| 1669 |
+ |
|
| 1670 |
+ fn save_entitlement_edit_from_button( |
|
| 1671 |
+ &mut self, |
|
| 1672 |
+ _: &ClickEvent, |
|
| 1673 |
+ window: &mut Window, |
|
| 1674 |
+ cx: &mut Context<Self>, |
|
| 1675 |
+ ) {
|
|
| 1676 |
+ cx.stop_propagation(); |
|
| 1677 |
+ window.focus(&self.focus_handle, cx); |
|
| 1678 |
+ self.save_entitlement_edit(cx); |
|
| 995 | 1679 |
} |
| 996 | 1680 |
|
| 997 |
-fn settings_app_id_row(app_id: &AppIdOption, selected: bool) -> gpui::Div {
|
|
| 998 |
- div() |
|
| 999 |
- .h(px(COMBO_ITEM_HEIGHT)) |
|
| 1000 |
- .px_3() |
|
| 1001 |
- .rounded_md() |
|
| 1002 |
- .border_1() |
|
| 1003 |
- .border_color(if selected {
|
|
| 1004 |
- rgb(0x0f6f7a) |
|
| 1005 |
- } else {
|
|
| 1006 |
- rgb(0xd8e0df) |
|
| 1007 |
- }) |
|
| 1008 |
- .bg(if selected {
|
|
| 1009 |
- rgb(0xf0fbfa) |
|
| 1681 |
+ fn cancel_entitlement_edit( |
|
| 1682 |
+ &mut self, |
|
| 1683 |
+ _: &ClickEvent, |
|
| 1684 |
+ window: &mut Window, |
|
| 1685 |
+ cx: &mut Context<Self>, |
|
| 1686 |
+ ) {
|
|
| 1687 |
+ cx.stop_propagation(); |
|
| 1688 |
+ window.focus(&self.focus_handle, cx); |
|
| 1689 |
+ self.entitlement_type_picker_open = false; |
|
| 1690 |
+ self.entitlement_edit = None; |
|
| 1691 |
+ cx.notify(); |
|
| 1692 |
+ } |
|
| 1693 |
+ |
|
| 1694 |
+ fn handle_entitlement_editor_key( |
|
| 1695 |
+ &mut self, |
|
| 1696 |
+ event: &KeyDownEvent, |
|
| 1697 |
+ window: &mut Window, |
|
| 1698 |
+ cx: &mut Context<Self>, |
|
| 1699 |
+ ) {
|
|
| 1700 |
+ match event.keystroke.key.as_str() {
|
|
| 1701 |
+ "enter" => {
|
|
| 1702 |
+ cx.stop_propagation(); |
|
| 1703 |
+ window.prevent_default(); |
|
| 1704 |
+ self.save_entitlement_edit(cx); |
|
| 1705 |
+ } |
|
| 1706 |
+ "escape" => {
|
|
| 1707 |
+ cx.stop_propagation(); |
|
| 1708 |
+ window.prevent_default(); |
|
| 1709 |
+ self.entitlement_type_picker_open = false; |
|
| 1710 |
+ self.entitlement_edit = None; |
|
| 1711 |
+ cx.notify(); |
|
| 1712 |
+ } |
|
| 1713 |
+ _ => {}
|
|
| 1714 |
+ } |
|
| 1715 |
+ } |
|
| 1716 |
+ |
|
| 1717 |
+ fn save_entitlement_edit(&mut self, cx: &mut Context<Self>) {
|
|
| 1718 |
+ let Some(edit) = self.entitlement_edit.take() else {
|
|
| 1719 |
+ return; |
|
| 1720 |
+ }; |
|
| 1721 |
+ self.entitlement_type_picker_open = false; |
|
| 1722 |
+ let SettingsWindowRequest::AppSettings {
|
|
| 1723 |
+ parent, |
|
| 1724 |
+ app_index, |
|
| 1725 |
+ app, |
|
| 1726 |
+ team_id, |
|
| 1727 |
+ .. |
|
| 1728 |
+ } = &mut self.request |
|
| 1729 |
+ else {
|
|
| 1730 |
+ cx.notify(); |
|
| 1731 |
+ return; |
|
| 1732 |
+ }; |
|
| 1733 |
+ |
|
| 1734 |
+ let row = edit.row(); |
|
| 1735 |
+ let mut entitlements = app.effective_entitlements(team_id); |
|
| 1736 |
+ let Some(entitlement) = entitlements.get_mut(row) else {
|
|
| 1737 |
+ cx.notify(); |
|
| 1738 |
+ return; |
|
| 1739 |
+ }; |
|
| 1740 |
+ |
|
| 1741 |
+ match edit {
|
|
| 1742 |
+ EntitlementEdit::Text { field, input, .. } => {
|
|
| 1743 |
+ let value = input.read(cx).value(); |
|
| 1744 |
+ match field {
|
|
| 1745 |
+ EntitlementField::Key => entitlement.key = value.into(), |
|
| 1746 |
+ EntitlementField::ValueType => {
|
|
| 1747 |
+ entitlement.value = entitlement.value.with_type_label(value.as_ref()) |
|
| 1748 |
+ } |
|
| 1749 |
+ EntitlementField::Value => {
|
|
| 1750 |
+ entitlement.value = entitlement.value.with_edit_text(value.as_ref()) |
|
| 1751 |
+ } |
|
| 1752 |
+ } |
|
| 1753 |
+ } |
|
| 1754 |
+ EntitlementEdit::Boolean { value, .. } => {
|
|
| 1755 |
+ entitlement.value = EntitlementValue::Boolean(value); |
|
| 1756 |
+ } |
|
| 1757 |
+ EntitlementEdit::Array { items, .. } => {
|
|
| 1758 |
+ entitlement.value = EntitlementValue::string_array( |
|
| 1759 |
+ items |
|
| 1760 |
+ .iter() |
|
| 1761 |
+ .map(|input| input.read(cx).value().to_string()) |
|
| 1762 |
+ .collect(), |
|
| 1763 |
+ ); |
|
| 1764 |
+ } |
|
| 1765 |
+ EntitlementEdit::Type { selected, .. } => {
|
|
| 1766 |
+ entitlement.value = entitlement.value.with_type_label(selected.as_ref()); |
|
| 1767 |
+ } |
|
| 1768 |
+ } |
|
| 1769 |
+ |
|
| 1770 |
+ app.entitlement_overrides = Some(entitlements); |
|
| 1771 |
+ self.selected_entitlement = Some(row); |
|
| 1772 |
+ let app_index = *app_index; |
|
| 1773 |
+ let updated = app.clone(); |
|
| 1774 |
+ let _ = parent.update(cx, |view, cx| {
|
|
| 1775 |
+ view.replace_app_from_settings(app_index, updated, cx); |
|
| 1776 |
+ }); |
|
| 1777 |
+ cx.notify(); |
|
| 1778 |
+ } |
|
| 1779 |
+} |
|
| 1780 |
+ |
|
| 1781 |
+impl Render for SettingsWindow {
|
|
| 1782 |
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
| 1783 |
+ if self.adi_operation.is_some() {
|
|
| 1784 |
+ self.spinner_turns = (self.spinner_turns + 0.035) % 1.; |
|
| 1785 |
+ window.request_animation_frame(); |
|
| 1010 | 1786 |
} else {
|
| 1011 |
- rgb(0xffffff) |
|
| 1012 |
- }) |
|
| 1013 |
- .cursor_pointer() |
|
| 1014 |
- .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 1015 |
- .flex() |
|
| 1016 |
- .items_center() |
|
| 1017 |
- .child(combo_item_content( |
|
| 1018 |
- app_id.identifier.clone(), |
|
| 1019 |
- app_id.name.clone(), |
|
| 1020 |
- app_id.kind.clone(), |
|
| 1021 |
- )) |
|
| 1787 |
+ self.spinner_turns = 0.; |
|
| 1022 | 1788 |
} |
| 1023 | 1789 |
|
| 1024 |
-fn settings_backend_combo_with_popover( |
|
| 1025 |
- trigger: impl IntoElement, |
|
| 1026 |
- open: bool, |
|
| 1027 |
- popover: impl IntoElement, |
|
| 1028 |
-) -> gpui::Div {
|
|
| 1029 |
- div().relative().child(trigger).when(open, |this| {
|
|
| 1030 |
- this.child( |
|
| 1031 |
- div().absolute().top_0().left_0().w(px(0.)).h(px(0.)).child( |
|
| 1032 |
- deferred( |
|
| 1033 |
- anchored() |
|
| 1034 |
- .snap_to_window_with_margin(px(8.)) |
|
| 1035 |
- .position_mode(AnchoredPositionMode::Local) |
|
| 1036 |
- .position(point(px(0.), px(ADI_BACKEND_COMBO_HEIGHT + 8.))) |
|
| 1037 |
- .child( |
|
| 1038 |
- div() |
|
| 1039 |
- .w(px(ADI_BACKEND_POPOVER_WIDTH)) |
|
| 1040 |
- .max_h(px(ADI_BACKEND_POPOVER_MAX_HEIGHT)) |
|
| 1041 |
- .rounded_md() |
|
| 1042 |
- .border_1() |
|
| 1043 |
- .border_color(rgb(0xd8e0df)) |
|
| 1044 |
- .bg(rgb(0xf8fbfa)) |
|
| 1045 |
- .id("adi-backend-popover-scroll")
|
|
| 1046 |
- .overflow_y_scroll() |
|
| 1047 |
- .scrollbar_width(px(8.)) |
|
| 1048 |
- .occlude() |
|
| 1049 |
- .child(popover), |
|
| 1790 |
+ let team_picker_open = self.team_picker_open; |
|
| 1791 |
+ let app_id_picker_open = self.app_id_picker_open; |
|
| 1792 |
+ let adi_operation = self.adi_operation.clone(); |
|
| 1793 |
+ let adi_busy = adi_operation.is_some(); |
|
| 1794 |
+ let adi_backend_picker_open = self.adi_backend_picker_open && !adi_busy; |
|
| 1795 |
+ let spinner_turns = self.spinner_turns; |
|
| 1796 |
+ let machine_identity_edit = self.machine_identity_edit.clone(); |
|
| 1797 |
+ let app_detail_edit = self.app_detail_edit.clone(); |
|
| 1798 |
+ let selected_entitlement = self.selected_entitlement; |
|
| 1799 |
+ let entitlement_edit = self.entitlement_edit.clone(); |
|
| 1800 |
+ let entitlement_type_picker_open = self.entitlement_type_picker_open; |
|
| 1801 |
+ |
|
| 1802 |
+ match &mut self.request {
|
|
| 1803 |
+ SettingsWindowRequest::DeveloperLogin { .. } => developer_login_view::render(
|
|
| 1804 |
+ &self.focus_handle, |
|
| 1805 |
+ &self.scroll_handle, |
|
| 1806 |
+ &self.developer_login, |
|
| 1807 |
+ cx, |
|
| 1050 | 1808 |
), |
| 1051 |
- ) |
|
| 1052 |
- .with_priority(10), |
|
| 1809 |
+ SettingsWindowRequest::Team {
|
|
| 1810 |
+ teams, |
|
| 1811 |
+ selected_team, |
|
| 1812 |
+ auto_app_id, |
|
| 1813 |
+ selected_app_id, |
|
| 1814 |
+ .. |
|
| 1815 |
+ } => developer_view::render( |
|
| 1816 |
+ &self.focus_handle, |
|
| 1817 |
+ &self.scroll_handle, |
|
| 1818 |
+ teams, |
|
| 1819 |
+ *selected_team, |
|
| 1820 |
+ *auto_app_id, |
|
| 1821 |
+ *selected_app_id, |
|
| 1822 |
+ team_picker_open, |
|
| 1823 |
+ app_id_picker_open, |
|
| 1824 |
+ cx, |
|
| 1053 | 1825 |
), |
| 1054 |
- ) |
|
| 1055 |
- }) |
|
| 1056 |
-} |
|
| 1057 |
- |
|
| 1058 |
-fn settings_adi_backend_combo_button(backend: &AdiBackendOption, open: bool) -> gpui::Div {
|
|
| 1059 |
- div() |
|
| 1060 |
- .h(px(ADI_BACKEND_COMBO_HEIGHT)) |
|
| 1061 |
- .p_3() |
|
| 1062 |
- .rounded_md() |
|
| 1063 |
- .border_1() |
|
| 1064 |
- .border_color(if open { rgb(0x0f6f7a) } else { rgb(0xcfd8d6) })
|
|
| 1065 |
- .bg(rgb(0xffffff)) |
|
| 1066 |
- .cursor_pointer() |
|
| 1067 |
- .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 1068 |
- .flex() |
|
| 1069 |
- .items_center() |
|
| 1070 |
- .gap_3() |
|
| 1071 |
- .child(status_dot(&backend.status)) |
|
| 1072 |
- .child( |
|
| 1073 |
- div() |
|
| 1074 |
- .min_w_0() |
|
| 1075 |
- .flex() |
|
| 1076 |
- .flex_1() |
|
| 1077 |
- .flex_col() |
|
| 1078 |
- .gap_1() |
|
| 1079 |
- .child( |
|
| 1080 |
- div() |
|
| 1081 |
- .text_sm() |
|
| 1082 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 1083 |
- .text_color(rgb(0x24333a)) |
|
| 1084 |
- .text_ellipsis() |
|
| 1085 |
- .child(backend.name.clone()), |
|
| 1086 |
- ) |
|
| 1087 |
- .child( |
|
| 1088 |
- div() |
|
| 1089 |
- .text_xs() |
|
| 1090 |
- .text_color(rgb(0x6a7a81)) |
|
| 1091 |
- .text_ellipsis() |
|
| 1092 |
- .child(backend.detail.clone()), |
|
| 1826 |
+ SettingsWindowRequest::AppSettings {
|
|
| 1827 |
+ app, |
|
| 1828 |
+ enabled_patches, |
|
| 1829 |
+ team_id, |
|
| 1830 |
+ .. |
|
| 1831 |
+ } => app_view::render( |
|
| 1832 |
+ &self.focus_handle, |
|
| 1833 |
+ &self.scroll_handle, |
|
| 1834 |
+ app, |
|
| 1835 |
+ enabled_patches, |
|
| 1836 |
+ team_id, |
|
| 1837 |
+ app_detail_edit.as_ref(), |
|
| 1838 |
+ selected_entitlement, |
|
| 1839 |
+ entitlement_edit.as_ref(), |
|
| 1840 |
+ entitlement_type_picker_open, |
|
| 1841 |
+ cx, |
|
| 1093 | 1842 |
), |
| 1094 |
- ) |
|
| 1095 |
- .child(status_badge(backend.status.clone(), false)) |
|
| 1096 |
- .child(chevron(open)) |
|
| 1843 |
+ SettingsWindowRequest::AdiSettings {
|
|
| 1844 |
+ backends, |
|
| 1845 |
+ selected_backend, |
|
| 1846 |
+ machine_identity, |
|
| 1847 |
+ android_device_identity, |
|
| 1848 |
+ android_adi_identifier: _, |
|
| 1849 |
+ .. |
|
| 1850 |
+ } => adi_view::render( |
|
| 1851 |
+ &self.focus_handle, |
|
| 1852 |
+ &self.scroll_handle, |
|
| 1853 |
+ backends, |
|
| 1854 |
+ *selected_backend, |
|
| 1855 |
+ machine_identity, |
|
| 1856 |
+ android_device_identity, |
|
| 1857 |
+ adi_backend_picker_open, |
|
| 1858 |
+ adi_operation.as_ref(), |
|
| 1859 |
+ spinner_turns, |
|
| 1860 |
+ machine_identity_edit.as_ref(), |
|
| 1861 |
+ cx, |
|
| 1862 |
+ ), |
|
| 1863 |
+ } |
|
| 1864 |
+ } |
|
| 1097 | 1865 |
} |
| 1098 | 1866 |
|
| 1099 |
-fn settings_adi_backend_option(backend: &AdiBackendOption, selected: bool) -> gpui::Div {
|
|
| 1100 |
- div() |
|
| 1101 |
- .min_h(px(56.)) |
|
| 1102 |
- .p_2() |
|
| 1103 |
- .rounded_md() |
|
| 1104 |
- .border_1() |
|
| 1105 |
- .border_color(if selected {
|
|
| 1106 |
- rgb(0x0f6f7a) |
|
| 1107 |
- } else {
|
|
| 1108 |
- rgb(0xd8e0df) |
|
| 1109 |
- }) |
|
| 1110 |
- .bg(if selected {
|
|
| 1111 |
- rgb(0xf0fbfa) |
|
| 1112 |
- } else {
|
|
| 1113 |
- rgb(0xffffff) |
|
| 1867 |
+pub(crate) fn show_or_open_settings_window( |
|
| 1868 |
+ handle: Option<WindowHandle<SettingsWindow>>, |
|
| 1869 |
+ request: SettingsWindowRequest, |
|
| 1870 |
+ width: f32, |
|
| 1871 |
+ height: f32, |
|
| 1872 |
+ cx: &mut App, |
|
| 1873 |
+) -> WindowHandle<SettingsWindow> {
|
|
| 1874 |
+ if let Some(handle) = handle {
|
|
| 1875 |
+ if handle |
|
| 1876 |
+ .update(cx, |settings, window, cx| {
|
|
| 1877 |
+ settings.show_request(request.clone(), window, cx); |
|
| 1114 | 1878 |
}) |
| 1115 |
- .cursor_pointer() |
|
| 1116 |
- .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 1117 |
- .flex() |
|
| 1118 |
- .items_center() |
|
| 1119 |
- .gap_2() |
|
| 1120 |
- .child(status_dot(&backend.status)) |
|
| 1121 |
- .child( |
|
| 1122 |
- div() |
|
| 1123 |
- .min_w_0() |
|
| 1124 |
- .flex() |
|
| 1125 |
- .flex_1() |
|
| 1126 |
- .flex_col() |
|
| 1127 |
- .gap_0p5() |
|
| 1128 |
- .child( |
|
| 1129 |
- div() |
|
| 1130 |
- .text_sm() |
|
| 1131 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 1132 |
- .text_color(rgb(0x24333a)) |
|
| 1133 |
- .text_ellipsis() |
|
| 1134 |
- .child(backend.name.clone()), |
|
| 1135 |
- ) |
|
| 1136 |
- .child( |
|
| 1137 |
- div() |
|
| 1138 |
- .text_xs() |
|
| 1139 |
- .text_color(rgb(0x6a7a81)) |
|
| 1140 |
- .text_ellipsis() |
|
| 1141 |
- .child(backend.detail.clone()), |
|
| 1142 |
- ), |
|
| 1143 |
- ) |
|
| 1144 |
- .child(status_badge(backend.status.clone(), selected)) |
|
| 1879 |
+ .is_ok() |
|
| 1880 |
+ {
|
|
| 1881 |
+ return handle; |
|
| 1882 |
+ } |
|
| 1145 | 1883 |
} |
| 1146 | 1884 |
|
| 1147 |
-fn adi_backend_status_section( |
|
| 1148 |
- backend: &AdiBackendOption, |
|
| 1149 |
- cx: &mut Context<SettingsWindow>, |
|
| 1150 |
-) -> gpui::Div {
|
|
| 1151 |
- let color = backend_status_color(&backend.status); |
|
| 1885 |
+ open_settings_window(request, width, height, cx) |
|
| 1886 |
+} |
|
| 1152 | 1887 |
|
| 1153 |
- div() |
|
| 1154 |
- .p_3() |
|
| 1155 |
- .rounded_md() |
|
| 1156 |
- .border_1() |
|
| 1157 |
- .border_color(rgb(0xd8e0df)) |
|
| 1158 |
- .bg(rgb(0xffffff)) |
|
| 1159 |
- .flex() |
|
| 1160 |
- .items_start() |
|
| 1161 |
- .gap_3() |
|
| 1162 |
- .child( |
|
| 1163 |
- div() |
|
| 1164 |
- .flex_none() |
|
| 1165 |
- .w(px(4.)) |
|
| 1166 |
- .h_full() |
|
| 1167 |
- .min_h(px(76.)) |
|
| 1168 |
- .rounded_full() |
|
| 1169 |
- .bg(rgb(color)), |
|
| 1170 |
- ) |
|
| 1171 |
- .child( |
|
| 1172 |
- div() |
|
| 1173 |
- .min_w_0() |
|
| 1174 |
- .flex() |
|
| 1175 |
- .flex_1() |
|
| 1176 |
- .flex_col() |
|
| 1177 |
- .gap_2() |
|
| 1178 |
- .child( |
|
| 1179 |
- div() |
|
| 1180 |
- .min_w_0() |
|
| 1181 |
- .flex() |
|
| 1182 |
- .flex_wrap() |
|
| 1183 |
- .items_center() |
|
| 1184 |
- .justify_between() |
|
| 1185 |
- .gap_2() |
|
| 1186 |
- .child( |
|
| 1187 |
- div() |
|
| 1188 |
- .min_w_0() |
|
| 1189 |
- .flex() |
|
| 1190 |
- .items_center() |
|
| 1191 |
- .gap_2() |
|
| 1192 |
- .child(status_dot(&backend.status)) |
|
| 1193 |
- .child( |
|
| 1194 |
- div() |
|
| 1195 |
- .text_sm() |
|
| 1196 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 1197 |
- .text_color(rgb(0x24333a)) |
|
| 1198 |
- .child("Status"),
|
|
| 1199 |
- ), |
|
| 1200 |
- ) |
|
| 1201 |
- .child(status_badge(backend.status.clone(), false)), |
|
| 1202 |
- ) |
|
| 1203 |
- .child( |
|
| 1204 |
- div() |
|
| 1205 |
- .min_w_0() |
|
| 1206 |
- .w_full() |
|
| 1207 |
- .text_xs() |
|
| 1208 |
- .line_height(px(18.)) |
|
| 1209 |
- .text_color(rgb(0x6a7a81)) |
|
| 1210 |
- .whitespace_normal() |
|
| 1211 |
- .child(backend.information.clone()), |
|
| 1212 |
- ) |
|
| 1213 |
- .when(backend.repair_action.is_some(), |this| {
|
|
| 1214 |
- let label = backend |
|
| 1215 |
- .repair_action |
|
| 1216 |
- .clone() |
|
| 1217 |
- .unwrap_or_else(|| "Fix Issues".into()); |
|
| 1218 |
- this.child( |
|
| 1219 |
- div() |
|
| 1220 |
- .id("repair-adi-backend")
|
|
| 1221 |
- .h_8() |
|
| 1222 |
- .px_3() |
|
| 1223 |
- .rounded_md() |
|
| 1224 |
- .bg(rgb(color)) |
|
| 1225 |
- .text_color(rgb(0xffffff)) |
|
| 1226 |
- .text_xs() |
|
| 1227 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 1228 |
- .flex() |
|
| 1229 |
- .items_center() |
|
| 1230 |
- .justify_center() |
|
| 1231 |
- .cursor_pointer() |
|
| 1232 |
- .hover(|style| style.opacity(0.88)) |
|
| 1233 |
- .on_click(cx.listener(SettingsWindow::repair_adi_backend)) |
|
| 1234 |
- .child(label), |
|
| 1235 |
- ) |
|
| 1236 |
- }), |
|
| 1888 |
+fn open_settings_window( |
|
| 1889 |
+ request: SettingsWindowRequest, |
|
| 1890 |
+ width: f32, |
|
| 1891 |
+ height: f32, |
|
| 1892 |
+ cx: &mut App, |
|
| 1893 |
+) -> WindowHandle<SettingsWindow> {
|
|
| 1894 |
+ let window_size = size(px(width), px(height)); |
|
| 1895 |
+ let bounds = Bounds::centered(None, window_size, cx); |
|
| 1896 |
+ let title = request.title(); |
|
| 1897 |
+ |
|
| 1898 |
+ cx.open_window( |
|
| 1899 |
+ WindowOptions {
|
|
| 1900 |
+ window_bounds: Some(WindowBounds::Windowed(bounds)), |
|
| 1901 |
+ window_min_size: Some(window_size), |
|
| 1902 |
+ is_resizable: true, |
|
| 1903 |
+ is_minimizable: false, |
|
| 1904 |
+ kind: WindowKind::Floating, |
|
| 1905 |
+ ..Default::default() |
|
| 1906 |
+ }, |
|
| 1907 |
+ |window, cx| {
|
|
| 1908 |
+ window.set_window_title(title); |
|
| 1909 |
+ cx.new(|cx| SettingsWindow::new(request, window, cx)) |
|
| 1910 |
+ }, |
|
| 1237 | 1911 |
) |
| 1912 |
+ .expect("failed to open settings window")
|
|
| 1238 | 1913 |
} |
| 1239 | 1914 |
|
| 1240 |
-fn machine_identity_section( |
|
| 1241 |
- identity: &MachineIdentity, |
|
| 1242 |
- editable: bool, |
|
| 1243 |
- cx: &mut Context<SettingsWindow>, |
|
| 1244 |
-) -> gpui::Div {
|
|
| 1245 |
- div() |
|
| 1246 |
- .p_3() |
|
| 1247 |
- .rounded_md() |
|
| 1248 |
- .border_1() |
|
| 1249 |
- .border_color(rgb(0xd8e0df)) |
|
| 1250 |
- .bg(rgb(0xffffff)) |
|
| 1251 |
- .flex() |
|
| 1252 |
- .flex_col() |
|
| 1253 |
- .gap_3() |
|
| 1254 |
- .child( |
|
| 1255 |
- div() |
|
| 1256 |
- .flex() |
|
| 1257 |
- .items_center() |
|
| 1258 |
- .gap_2() |
|
| 1259 |
- .child( |
|
| 1260 |
- div() |
|
| 1261 |
- .text_sm() |
|
| 1262 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 1263 |
- .text_color(rgb(0x24333a)) |
|
| 1264 |
- .child("Machine Identity"),
|
|
| 1915 |
+fn open_app_data_folder() -> Result<(), String> {
|
|
| 1916 |
+ let data_dir = app_data_dir() |
|
| 1917 |
+ .ok_or_else(|| "The application data folder is not available.".to_string())?; |
|
| 1918 |
+ fs::create_dir_all(&data_dir).map_err(|error| {
|
|
| 1919 |
+ format!( |
|
| 1920 |
+ "Failed to create application data folder at {}: {error}",
|
|
| 1921 |
+ data_dir.display() |
|
| 1265 | 1922 |
) |
| 1266 |
- .when(!editable, |this| this.child(read_only_badge())), |
|
| 1923 |
+ })?; |
|
| 1924 |
+ |
|
| 1925 |
+ let mut command = if cfg!(target_os = "macos") {
|
|
| 1926 |
+ Command::new("open")
|
|
| 1927 |
+ } else if cfg!(target_os = "windows") {
|
|
| 1928 |
+ Command::new("explorer")
|
|
| 1929 |
+ } else {
|
|
| 1930 |
+ Command::new("xdg-open")
|
|
| 1931 |
+ }; |
|
| 1932 |
+ |
|
| 1933 |
+ command.arg(&data_dir).spawn().map_err(|error| {
|
|
| 1934 |
+ format!( |
|
| 1935 |
+ "Failed to open application data folder at {}: {error}",
|
|
| 1936 |
+ data_dir.display() |
|
| 1267 | 1937 |
) |
| 1268 |
- .child(machine_identity_row( |
|
| 1269 |
- "Machine name", |
|
| 1270 |
- identity.machine_name.clone(), |
|
| 1271 |
- editable, |
|
| 1272 |
- cx, |
|
| 1273 |
- )) |
|
| 1274 |
- .child(machine_identity_row( |
|
| 1275 |
- "OS name", |
|
| 1276 |
- identity.os_name.clone(), |
|
| 1277 |
- editable, |
|
| 1278 |
- cx, |
|
| 1279 |
- )) |
|
| 1280 |
- .child(machine_identity_row( |
|
| 1281 |
- "OS version", |
|
| 1282 |
- identity.os_version.clone(), |
|
| 1283 |
- editable, |
|
| 1284 |
- cx, |
|
| 1285 |
- )) |
|
| 1286 |
- .child(machine_identity_row( |
|
| 1287 |
- "Machine ID", |
|
| 1288 |
- identity.machine_id.clone(), |
|
| 1289 |
- editable, |
|
| 1290 |
- cx, |
|
| 1291 |
- )) |
|
| 1938 |
+ })?; |
|
| 1939 |
+ Ok(()) |
|
| 1292 | 1940 |
} |
| 1293 | 1941 |
|
| 1294 |
-fn machine_identity_row( |
|
| 1295 |
- label: &'static str, |
|
| 1296 |
- value: SharedString, |
|
| 1297 |
- editable: bool, |
|
| 1298 |
- cx: &mut Context<SettingsWindow>, |
|
| 1942 |
+fn scroll_panel( |
|
| 1943 |
+ scroll_id: &'static str, |
|
| 1944 |
+ scroll_handle: &ScrollHandle, |
|
| 1945 |
+ content: impl IntoElement, |
|
| 1299 | 1946 |
) -> gpui::Div {
|
| 1947 |
+ let viewport_height = scroll_handle.bounds().size.height.as_f32(); |
|
| 1948 |
+ let max_scroll = scroll_handle.max_offset().y.as_f32(); |
|
| 1949 |
+ let is_scrollable = viewport_height > 0. && max_scroll > 0.5; |
|
| 1950 |
+ let scroll_offset = (-scroll_handle.offset().y.as_f32()).clamp(0., max_scroll); |
|
| 1951 |
+ let content_height = viewport_height + max_scroll; |
|
| 1952 |
+ let thumb_height = if is_scrollable {
|
|
| 1953 |
+ ((viewport_height / content_height) * viewport_height).clamp(36., viewport_height) |
|
| 1954 |
+ } else {
|
|
| 1955 |
+ 0. |
|
| 1956 |
+ }; |
|
| 1957 |
+ let thumb_top = if is_scrollable && max_scroll > 0. {
|
|
| 1958 |
+ (scroll_offset / max_scroll) * (viewport_height - thumb_height) |
|
| 1959 |
+ } else {
|
|
| 1960 |
+ 0. |
|
| 1961 |
+ }; |
|
| 1962 |
+ |
|
| 1300 | 1963 |
div() |
| 1301 |
- .min_w_0() |
|
| 1964 |
+ .min_h_0() |
|
| 1965 |
+ .flex_1() |
|
| 1302 | 1966 |
.flex() |
| 1303 |
- .items_center() |
|
| 1304 |
- .gap_3() |
|
| 1305 |
- .child( |
|
| 1306 |
- div() |
|
| 1307 |
- .w(px(104.)) |
|
| 1308 |
- .flex_none() |
|
| 1309 |
- .text_xs() |
|
| 1310 |
- .text_color(rgb(0x7b8a90)) |
|
| 1311 |
- .child(label), |
|
| 1312 |
- ) |
|
| 1967 |
+ .gap_2() |
|
| 1313 | 1968 |
.child( |
| 1314 | 1969 |
div() |
| 1970 |
+ .id(scroll_id) |
|
| 1315 | 1971 |
.min_w_0() |
| 1972 |
+ .h_full() |
|
| 1316 | 1973 |
.flex_1() |
| 1317 |
- .text_sm() |
|
| 1318 |
- .text_color(rgb(0x405057)) |
|
| 1319 |
- .text_ellipsis() |
|
| 1320 |
- .child(value), |
|
| 1974 |
+ .overflow_y_scroll() |
|
| 1975 |
+ .scrollbar_width(px(1.)) |
|
| 1976 |
+ .track_scroll(scroll_handle) |
|
| 1977 |
+ .child(content), |
|
| 1321 | 1978 |
) |
| 1322 |
- .when(editable, |this| {
|
|
| 1979 |
+ .when(is_scrollable, |this| {
|
|
| 1323 | 1980 |
this.child( |
| 1324 |
- div() |
|
| 1325 |
- .id(("edit-machine-identity", machine_identity_id(label)))
|
|
| 1326 |
- .flex_none() |
|
| 1327 |
- .w_6() |
|
| 1328 |
- .h_6() |
|
| 1329 |
- .rounded_md() |
|
| 1330 |
- .bg(rgb(0xebf1f0)) |
|
| 1331 |
- .text_color(rgb(0x53666d)) |
|
| 1332 |
- .flex() |
|
| 1333 |
- .items_center() |
|
| 1334 |
- .justify_center() |
|
| 1335 |
- .cursor_pointer() |
|
| 1336 |
- .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 1337 |
- .on_click(cx.listener(SettingsWindow::edit_machine_identity)) |
|
| 1338 |
- .child(lucide_icon("icons/pencil.svg")),
|
|
| 1339 |
- ) |
|
| 1340 |
- }) |
|
| 1341 |
-} |
|
| 1342 |
- |
|
| 1343 |
-fn machine_identity_id(label: &'static str) -> usize {
|
|
| 1344 |
- match label {
|
|
| 1345 |
- "Machine name" => 0, |
|
| 1346 |
- "OS name" => 1, |
|
| 1347 |
- "OS version" => 2, |
|
| 1348 |
- "Machine ID" => 3, |
|
| 1349 |
- _ => 99, |
|
| 1350 |
- } |
|
| 1351 |
-} |
|
| 1352 |
- |
|
| 1353 |
-fn status_badge(label: SharedString, selected: bool) -> gpui::Div {
|
|
| 1354 |
- let color = backend_status_color(&label); |
|
| 1355 |
- |
|
| 1356 | 1981 |
div() |
| 1357 | 1982 |
.flex_none() |
| 1358 |
- .px_2() |
|
| 1359 |
- .h_6() |
|
| 1983 |
+ .w(px(6.)) |
|
| 1984 |
+ .h_full() |
|
| 1360 | 1985 |
.rounded_full() |
| 1361 |
- .bg(if selected {
|
|
| 1362 |
- rgb(status_badge_selected_bg(&label)) |
|
| 1363 |
- } else {
|
|
| 1364 |
- rgb(status_badge_bg(&label)) |
|
| 1365 |
- }) |
|
| 1366 |
- .text_color(rgb(color)) |
|
| 1367 |
- .text_xs() |
|
| 1368 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 1986 |
+ .bg(rgb(0xdde5e4)) |
|
| 1369 | 1987 |
.flex() |
| 1370 |
- .items_center() |
|
| 1371 |
- .justify_center() |
|
| 1372 |
- .child(label) |
|
| 1373 |
-} |
|
| 1374 |
- |
|
| 1375 |
-fn status_dot(status: &SharedString) -> gpui::Div {
|
|
| 1988 |
+ .flex_col() |
|
| 1989 |
+ .child(div().flex_none().h(px(thumb_top))) |
|
| 1990 |
+ .child( |
|
| 1376 | 1991 |
div() |
| 1377 | 1992 |
.flex_none() |
| 1378 |
- .w_2() |
|
| 1379 |
- .h_2() |
|
| 1993 |
+ .w_full() |
|
| 1994 |
+ .h(px(thumb_height)) |
|
| 1380 | 1995 |
.rounded_full() |
| 1381 |
- .bg(rgb(backend_status_color(status))) |
|
| 1382 |
-} |
|
| 1383 |
- |
|
| 1384 |
-fn backend_status_color(status: &SharedString) -> u32 {
|
|
| 1385 |
- match status.to_string().as_str() {
|
|
| 1386 |
- "Ready" => 0x1d6b45, |
|
| 1387 |
- "Needs setup" => 0x9a6a14, |
|
| 1388 |
- "Offline" => 0x9a302b, |
|
| 1389 |
- _ => 0x53666d, |
|
| 1390 |
- } |
|
| 1391 |
-} |
|
| 1392 |
- |
|
| 1393 |
-fn status_badge_bg(status: &SharedString) -> u32 {
|
|
| 1394 |
- match status.to_string().as_str() {
|
|
| 1395 |
- "Ready" => 0xe5f3ec, |
|
| 1396 |
- "Needs setup" => 0xf6edda, |
|
| 1397 |
- "Offline" => 0xf4e4e2, |
|
| 1398 |
- _ => 0xebf1f0, |
|
| 1399 |
- } |
|
| 1400 |
-} |
|
| 1401 |
- |
|
| 1402 |
-fn status_badge_selected_bg(status: &SharedString) -> u32 {
|
|
| 1403 |
- match status.to_string().as_str() {
|
|
| 1404 |
- "Ready" => 0xd3eadc, |
|
| 1405 |
- "Needs setup" => 0xeadbb8, |
|
| 1406 |
- "Offline" => 0xebcecb, |
|
| 1407 |
- _ => 0xd7efec, |
|
| 1408 |
- } |
|
| 1996 |
+ .bg(rgb(0x789094)), |
|
| 1997 |
+ ) |
|
| 1998 |
+ .child(div().flex_1()), |
|
| 1999 |
+ ) |
|
| 2000 |
+ }) |
|
| 1409 | 2001 |
} |
| 1410 | 2002 |
|
| 1411 |
-fn read_only_badge() -> gpui::Div {
|
|
| 2003 |
+fn settings_window_shell() -> gpui::Div {
|
|
| 1412 | 2004 |
div() |
| 1413 |
- .px_2() |
|
| 1414 |
- .h_5() |
|
| 1415 |
- .rounded_full() |
|
| 1416 |
- .bg(rgb(0xf0f2f2)) |
|
| 1417 |
- .text_color(rgb(0x718086)) |
|
| 1418 |
- .text_xs() |
|
| 1419 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 2005 |
+ .size_full() |
|
| 2006 |
+ .p_5() |
|
| 1420 | 2007 |
.flex() |
| 1421 |
- .items_center() |
|
| 1422 |
- .justify_center() |
|
| 1423 |
- .child("Read-only")
|
|
| 2008 |
+ .flex_col() |
|
| 2009 |
+ .bg(rgb(0xf4f6f4)) |
|
| 2010 |
+ .text_color(rgb(0x263238)) |
|
| 2011 |
+ .font_family(".SystemUIFont")
|
|
| 1424 | 2012 |
} |
| 1425 | 2013 |
|
| 1426 |
-fn app_icon_editor(app: &AppOption, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 1427 |
- let initial = app |
|
| 1428 |
- .name |
|
| 1429 |
- .to_string() |
|
| 1430 |
- .chars() |
|
| 1431 |
- .next() |
|
| 1432 |
- .map(|c| c.to_string()) |
|
| 1433 |
- .unwrap_or_else(|| "A".to_string()); |
|
| 1434 |
- |
|
| 2014 |
+fn settings_window_header(title: &'static str, detail: &'static str) -> gpui::Div {
|
|
| 1435 | 2015 |
div() |
| 1436 |
- .flex_none() |
|
| 1437 |
- .w(px(84.)) |
|
| 1438 |
- .h(px(84.)) |
|
| 1439 |
- .rounded_md() |
|
| 1440 |
- .border_1() |
|
| 1441 |
- .border_color(rgb(0xcfd8d6)) |
|
| 1442 |
- .bg(rgb(0x173f45)) |
|
| 1443 |
- .text_color(rgb(0xffffff)) |
|
| 1444 |
- .cursor_pointer() |
|
| 1445 |
- .hover(|style| style.bg(rgb(0x20545c))) |
|
| 1446 | 2016 |
.flex() |
| 1447 | 2017 |
.flex_col() |
| 1448 |
- .items_center() |
|
| 1449 |
- .justify_center() |
|
| 1450 |
- .gap_1() |
|
| 1451 |
- .id("app-icon-editor")
|
|
| 1452 |
- .on_click(cx.listener(SettingsWindow::edit_app_icon)) |
|
| 2018 |
+ .gap_0p5() |
|
| 1453 | 2019 |
.child( |
| 1454 | 2020 |
div() |
| 1455 |
- .text_2xl() |
|
| 2021 |
+ .text_sm() |
|
| 1456 | 2022 |
.font_weight(FontWeight::BOLD) |
| 1457 |
- .child(initial), |
|
| 2023 |
+ .text_color(rgb(0x24333a)) |
|
| 2024 |
+ .child(title), |
|
| 1458 | 2025 |
) |
| 1459 |
- .child(div().text_xs().child("Edit icon"))
|
|
| 2026 |
+ .child(div().text_xs().text_color(rgb(0x6a7a81)).child(detail)) |
|
| 1460 | 2027 |
} |
| 1461 | 2028 |
|
| 1462 |
-fn app_detail_row( |
|
| 1463 |
- label: &'static str, |
|
| 1464 |
- value: String, |
|
| 1465 |
- editable: bool, |
|
| 1466 |
- cx: &mut Context<SettingsWindow>, |
|
| 2029 |
+fn settings_window_header_with_action( |
|
| 2030 |
+ header: impl IntoElement, |
|
| 2031 |
+ action: impl IntoElement, |
|
| 1467 | 2032 |
) -> gpui::Div {
|
| 1468 | 2033 |
div() |
| 1469 |
- .min_w_0() |
|
| 1470 | 2034 |
.flex() |
| 1471 |
- .items_center() |
|
| 2035 |
+ .items_start() |
|
| 2036 |
+ .justify_between() |
|
| 1472 | 2037 |
.gap_3() |
| 1473 |
- .child( |
|
| 1474 |
- div() |
|
| 1475 |
- .w(px(96.)) |
|
| 1476 |
- .flex_none() |
|
| 1477 |
- .text_xs() |
|
| 1478 |
- .text_color(rgb(0x7b8a90)) |
|
| 1479 |
- .child(label), |
|
| 1480 |
- ) |
|
| 1481 |
- .child( |
|
| 1482 |
- div() |
|
| 1483 |
- .min_w_0() |
|
| 1484 |
- .flex_1() |
|
| 1485 |
- .text_sm() |
|
| 1486 |
- .text_color(rgb(0x405057)) |
|
| 1487 |
- .text_ellipsis() |
|
| 1488 |
- .child(value), |
|
| 1489 |
- ) |
|
| 1490 |
- .when(editable, |this| this.child(app_detail_controls(label, cx))) |
|
| 1491 |
-} |
|
| 1492 |
- |
|
| 1493 |
-fn app_detail_controls(label: &'static str, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 1494 |
- div() |
|
| 1495 |
- .flex_none() |
|
| 1496 |
- .w(px(84.)) |
|
| 1497 |
- .flex() |
|
| 1498 |
- .items_center() |
|
| 1499 |
- .justify_end() |
|
| 1500 |
- .gap_1() |
|
| 1501 |
- .child(app_detail_revert_slot(label, false, cx)) |
|
| 1502 |
- .child(edit_button(label, cx)) |
|
| 2038 |
+ .child(header) |
|
| 2039 |
+ .child(action) |
|
| 1503 | 2040 |
} |
| 1504 | 2041 |
|
| 1505 |
-fn edit_button(label: &'static str, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 2042 |
+fn open_data_folder_button(cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 1506 | 2043 |
div() |
| 2044 |
+ .id("open-data-folder")
|
|
| 1507 | 2045 |
.flex_none() |
| 1508 |
- .w_6() |
|
| 1509 |
- .h_6() |
|
| 2046 |
+ .h_8() |
|
| 2047 |
+ .px_3() |
|
| 1510 | 2048 |
.rounded_md() |
| 1511 | 2049 |
.bg(rgb(0xebf1f0)) |
| 1512 | 2050 |
.text_color(rgb(0x53666d)) |
| 2051 |
+ .text_xs() |
|
| 2052 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 1513 | 2053 |
.flex() |
| 1514 | 2054 |
.items_center() |
| 1515 | 2055 |
.justify_center() |
| 2056 |
+ .gap_2() |
|
| 1516 | 2057 |
.cursor_pointer() |
| 1517 | 2058 |
.hover(|style| style.bg(rgb(0xdfe8e6))) |
| 1518 |
- .id(("edit-app-detail", app_detail_id(label)))
|
|
| 1519 |
- .on_click(cx.listener(SettingsWindow::edit_app_detail)) |
|
| 1520 |
- .child(lucide_icon("icons/pencil.svg"))
|
|
| 2059 |
+ .on_click(cx.listener(SettingsWindow::open_data_folder)) |
|
| 2060 |
+ .child(lucide_icon("icons/folder-open.svg"))
|
|
| 2061 |
+ .child("Open Data Folder")
|
|
| 1521 | 2062 |
} |
| 1522 | 2063 |
|
| 1523 |
-fn app_detail_revert_slot( |
|
| 1524 |
- label: &'static str, |
|
| 1525 |
- changed: bool, |
|
| 1526 |
- cx: &mut Context<SettingsWindow>, |
|
| 1527 |
-) -> impl IntoElement {
|
|
| 2064 |
+fn settings_label(label: &'static str) -> gpui::Div {
|
|
| 1528 | 2065 |
div() |
| 1529 |
- .flex_none() |
|
| 1530 |
- .w(px(52.)) |
|
| 1531 |
- .h_6() |
|
| 1532 |
- .rounded_md() |
|
| 1533 |
- .bg(rgb(0xebf1f0)) |
|
| 1534 |
- .text_color(rgb(0x53666d)) |
|
| 1535 | 2066 |
.text_xs() |
| 1536 | 2067 |
.font_weight(FontWeight::SEMIBOLD) |
| 1537 |
- .flex() |
|
| 1538 |
- .items_center() |
|
| 1539 |
- .justify_center() |
|
| 1540 |
- .id(("revert-app-detail", app_detail_id(label)))
|
|
| 1541 |
- .when(!changed, |this| this.opacity(0.)) |
|
| 1542 |
- .when(changed, |this| {
|
|
| 1543 |
- this.cursor_pointer() |
|
| 1544 |
- .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 1545 |
- .on_click(cx.listener(SettingsWindow::revert_app_detail)) |
|
| 1546 |
- }) |
|
| 1547 |
- .child("Revert")
|
|
| 1548 |
-} |
|
| 1549 |
- |
|
| 1550 |
-fn app_detail_id(label: &'static str) -> usize {
|
|
| 1551 |
- match label {
|
|
| 1552 |
- "Name" => 0, |
|
| 1553 |
- "Bundle ID" => 1, |
|
| 1554 |
- "Version" => 2, |
|
| 1555 |
- "Build" => 3, |
|
| 1556 |
- "Executable" => 4, |
|
| 1557 |
- "Minimum OS" => 5, |
|
| 1558 |
- "Supported devices" => 6, |
|
| 1559 |
- _ => 99, |
|
| 1560 |
- } |
|
| 1561 |
-} |
|
| 1562 |
- |
|
| 1563 |
-fn enabled_patch_summary(app: &AppOption, enabled_patches: &[bool]) -> String {
|
|
| 1564 |
- let enabled = app |
|
| 1565 |
- .patches |
|
| 1566 |
- .iter() |
|
| 1567 |
- .enumerate() |
|
| 1568 |
- .filter_map(|(index, patch)| {
|
|
| 1569 |
- enabled_patches |
|
| 1570 |
- .get(index) |
|
| 1571 |
- .copied() |
|
| 1572 |
- .filter(|enabled| *enabled) |
|
| 1573 |
- .map(|_| format!("{} ({})", patch.name, patch.detail))
|
|
| 1574 |
- }) |
|
| 1575 |
- .collect::<Vec<_>>(); |
|
| 1576 |
- |
|
| 1577 |
- if enabled.is_empty() {
|
|
| 1578 |
- "No patch changes selected".to_string() |
|
| 1579 |
- } else {
|
|
| 1580 |
- enabled.join(", ")
|
|
| 1581 |
- } |
|
| 2068 |
+ .text_color(rgb(0x66767c)) |
|
| 2069 |
+ .child(label) |
|
| 1582 | 2070 |
} |
| 1583 | 2071 |
|
| 1584 |
-fn entitlements_table(app: &AppOption, cx: &mut Context<SettingsWindow>) -> gpui::Div {
|
|
| 2072 |
+fn machine_identity_editor(input: &Entity<EditLine>) -> gpui::Div {
|
|
| 1585 | 2073 |
div() |
| 1586 |
- .flex() |
|
| 1587 |
- .flex_col() |
|
| 2074 |
+ .min_w_0() |
|
| 2075 |
+ .flex_1() |
|
| 2076 |
+ .h_7() |
|
| 2077 |
+ .px_2() |
|
| 1588 | 2078 |
.rounded_md() |
| 1589 | 2079 |
.border_1() |
| 1590 |
- .border_color(rgb(0xd8e0df)) |
|
| 1591 |
- .bg(rgb(0xffffff)) |
|
| 1592 |
- .overflow_hidden() |
|
| 1593 |
- .child( |
|
| 1594 |
- div() |
|
| 1595 |
- .px_3() |
|
| 1596 |
- .py_2() |
|
| 1597 |
- .border_b_1() |
|
| 1598 |
- .border_color(rgb(0xd8e0df)) |
|
| 1599 |
- .flex() |
|
| 1600 |
- .items_center() |
|
| 1601 |
- .justify_start() |
|
| 1602 |
- .gap_2() |
|
| 1603 |
- .child( |
|
| 1604 |
- div() |
|
| 2080 |
+ .border_color(rgb(0x168291)) |
|
| 2081 |
+ .bg(rgb(0xf2fbfb)) |
|
| 1605 | 2082 |
.text_sm() |
| 1606 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 1607 | 2083 |
.text_color(rgb(0x24333a)) |
| 1608 |
- .child("Entitlements"),
|
|
| 1609 |
- ) |
|
| 1610 |
- .child(entitlements_actions(false, cx)), |
|
| 1611 |
- ) |
|
| 1612 |
- .child(entitlement_header_row()) |
|
| 1613 |
- .child(entitlement_row( |
|
| 1614 |
- "application-identifier", |
|
| 1615 |
- "String", |
|
| 1616 |
- format!("TEAMID1234.{}", app.bundle_id),
|
|
| 1617 |
- )) |
|
| 1618 |
- .child(entitlement_row( |
|
| 1619 |
- "com.apple.developer.team-identifier", |
|
| 1620 |
- "String", |
|
| 1621 |
- "TEAMID1234".to_string(), |
|
| 1622 |
- )) |
|
| 1623 |
- .child(entitlement_row( |
|
| 1624 |
- "get-task-allow", |
|
| 1625 |
- "Boolean", |
|
| 1626 |
- "true".to_string(), |
|
| 1627 |
- )) |
|
| 1628 |
- .child(entitlement_row( |
|
| 1629 |
- "keychain-access-groups", |
|
| 1630 |
- "Array", |
|
| 1631 |
- format!("TEAMID1234.{}", app.bundle_id),
|
|
| 1632 |
- )) |
|
| 1633 |
- .child(entitlement_row( |
|
| 1634 |
- "com.apple.security.application-groups", |
|
| 1635 |
- "Array", |
|
| 1636 |
- "None".to_string(), |
|
| 1637 |
- )) |
|
| 1638 |
-} |
|
| 1639 |
- |
|
| 1640 |
-fn entitlements_actions(changed: bool, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 1641 |
- div() |
|
| 1642 |
- .flex_none() |
|
| 1643 |
- .flex() |
|
| 1644 |
- .items_center() |
|
| 1645 |
- .gap_1() |
|
| 1646 |
- .child(entitlement_icon_button( |
|
| 1647 |
- "add-entitlement", |
|
| 1648 |
- "icons/plus.svg", |
|
| 1649 |
- cx.listener(SettingsWindow::add_entitlement), |
|
| 1650 |
- )) |
|
| 1651 |
- .child(entitlement_icon_button( |
|
| 1652 |
- "remove-entitlement", |
|
| 1653 |
- "icons/minus.svg", |
|
| 1654 |
- cx.listener(SettingsWindow::remove_entitlement), |
|
| 1655 |
- )) |
|
| 1656 |
- .child(entitlements_revert_slot(changed, cx)) |
|
| 1657 |
-} |
|
| 1658 |
- |
|
| 1659 |
-fn entitlement_icon_button( |
|
| 1660 |
- id: &'static str, |
|
| 1661 |
- icon: &'static str, |
|
| 1662 |
- listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, |
|
| 1663 |
-) -> impl IntoElement {
|
|
| 1664 |
- div() |
|
| 1665 |
- .flex_none() |
|
| 1666 |
- .w_6() |
|
| 1667 |
- .h_6() |
|
| 1668 |
- .rounded_md() |
|
| 1669 |
- .bg(rgb(0xebf1f0)) |
|
| 1670 |
- .text_color(rgb(0x53666d)) |
|
| 1671 | 2084 |
.flex() |
| 1672 | 2085 |
.items_center() |
| 1673 |
- .justify_center() |
|
| 1674 |
- .cursor_pointer() |
|
| 1675 |
- .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 1676 |
- .id(id) |
|
| 1677 |
- .on_click(listener) |
|
| 1678 |
- .child(lucide_icon(icon)) |
|
| 2086 |
+ .overflow_hidden() |
|
| 2087 |
+ .child(input.clone()) |
|
| 1679 | 2088 |
} |
| 1680 | 2089 |
|
| 1681 |
-fn entitlements_revert_slot(changed: bool, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 1682 |
- div() |
|
| 1683 |
- .flex_none() |
|
| 1684 |
- .w(px(52.)) |
|
| 1685 |
- .h_6() |
|
| 1686 |
- .rounded_md() |
|
| 1687 |
- .bg(rgb(0xebf1f0)) |
|
| 1688 |
- .text_color(rgb(0x53666d)) |
|
| 1689 |
- .text_xs() |
|
| 1690 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 1691 |
- .flex() |
|
| 1692 |
- .items_center() |
|
| 1693 |
- .justify_center() |
|
| 1694 |
- .id("revert-entitlements")
|
|
| 1695 |
- .when(!changed, |this| this.opacity(0.)) |
|
| 1696 |
- .when(changed, |this| {
|
|
| 1697 |
- this.cursor_pointer() |
|
| 1698 |
- .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 1699 |
- .on_click(cx.listener(SettingsWindow::revert_entitlements)) |
|
| 1700 |
- }) |
|
| 1701 |
- .child("Revert")
|
|
| 2090 |
+fn entitlement_revert_target( |
|
| 2091 |
+ entitlement: &AppEntitlement, |
|
| 2092 |
+ row: usize, |
|
| 2093 |
+ defaults: &[AppEntitlement], |
|
| 2094 |
+) -> Option<AppEntitlement> {
|
|
| 2095 |
+ defaults |
|
| 2096 |
+ .iter() |
|
| 2097 |
+ .find(|default| default.key == entitlement.key) |
|
| 2098 |
+ .cloned() |
|
| 2099 |
+ .or_else(|| defaults.get(row).cloned()) |
|
| 1702 | 2100 |
} |
| 1703 | 2101 |
|
| 1704 |
-fn entitlement_header_row() -> gpui::Div {
|
|
| 1705 |
- div() |
|
| 1706 |
- .grid() |
|
| 1707 |
- .grid_cols(3) |
|
| 1708 |
- .px_3() |
|
| 1709 |
- .py_2() |
|
| 1710 |
- .bg(rgb(0xf6f9f9)) |
|
| 1711 |
- .border_b_1() |
|
| 1712 |
- .border_color(rgb(0xd8e0df)) |
|
| 1713 |
- .text_xs() |
|
| 1714 |
- .font_weight(FontWeight::SEMIBOLD) |
|
| 1715 |
- .text_color(rgb(0x66767c)) |
|
| 1716 |
- .child("Key")
|
|
| 1717 |
- .child("Type")
|
|
| 1718 |
- .child("Value")
|
|
| 2102 |
+fn entitlement_field_value(entitlement: &AppEntitlement, field: EntitlementField) -> SharedString {
|
|
| 2103 |
+ match field {
|
|
| 2104 |
+ EntitlementField::Key => entitlement.key.clone(), |
|
| 2105 |
+ EntitlementField::ValueType => entitlement.value.type_label().into(), |
|
| 2106 |
+ EntitlementField::Value => entitlement.value.display_text(), |
|
| 1719 | 2107 |
} |
| 1720 |
- |
|
| 1721 |
-fn entitlement_row(key: &'static str, kind: &'static str, value: String) -> gpui::Div {
|
|
| 1722 |
- div() |
|
| 1723 |
- .grid() |
|
| 1724 |
- .grid_cols(3) |
|
| 1725 |
- .px_3() |
|
| 1726 |
- .py_2() |
|
| 1727 |
- .border_b_1() |
|
| 1728 |
- .border_color(rgb(0xe8eeee)) |
|
| 1729 |
- .text_xs() |
|
| 1730 |
- .text_color(rgb(0x405057)) |
|
| 1731 |
- .child(div().min_w_0().text_ellipsis().child(key)) |
|
| 1732 |
- .child(div().min_w_0().text_ellipsis().child(kind)) |
|
| 1733 |
- .child(div().min_w_0().text_ellipsis().child(value)) |
|
| 1734 | 2108 |
} |
| ... | ... |
@@ -0,0 +1,770 @@ |
| 1 |
+use super::*; |
|
| 2 |
+ |
|
| 3 |
+pub(super) fn render( |
|
| 4 |
+ focus_handle: &FocusHandle, |
|
| 5 |
+ scroll_handle: &ScrollHandle, |
|
| 6 |
+ backends: &[AdiBackendOption], |
|
| 7 |
+ selected_backend: usize, |
|
| 8 |
+ machine_identity: &MachineIdentity, |
|
| 9 |
+ android_device_identity: &MachineIdentity, |
|
| 10 |
+ adi_backend_picker_open: bool, |
|
| 11 |
+ adi_operation: Option<&AdiOperationState>, |
|
| 12 |
+ spinner_turns: f32, |
|
| 13 |
+ machine_identity_edit: Option<&MachineIdentityEdit>, |
|
| 14 |
+ cx: &mut Context<SettingsWindow>, |
|
| 15 |
+) -> gpui::Div {
|
|
| 16 |
+ let selected = selected_backend.min(backends.len().saturating_sub(1)); |
|
| 17 |
+ let backend = backends.get(selected); |
|
| 18 |
+ let displayed_machine_identity: &MachineIdentity = match backend.map(|backend| backend.kind) {
|
|
| 19 |
+ Some(AdiBackendKind::AndroidCoreAdi) => android_device_identity, |
|
| 20 |
+ Some(AdiBackendKind::SystemAdid) | Some(AdiBackendKind::WindowsCoreAdi) | None => {
|
|
| 21 |
+ machine_identity |
|
| 22 |
+ } |
|
| 23 |
+ }; |
|
| 24 |
+ let editable_identity = backend |
|
| 25 |
+ .map(|backend| backend.editable_identity) |
|
| 26 |
+ .unwrap_or(false); |
|
| 27 |
+ let backend_controls_disabled = adi_operation.is_some(); |
|
| 28 |
+ |
|
| 29 |
+ let mut content = div().flex().flex_col().gap_4().child( |
|
| 30 |
+ div() |
|
| 31 |
+ .flex() |
|
| 32 |
+ .flex_col() |
|
| 33 |
+ .gap_2() |
|
| 34 |
+ .child( |
|
| 35 |
+ div() |
|
| 36 |
+ .text_xs() |
|
| 37 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 38 |
+ .text_color(rgb(0x66767c)) |
|
| 39 |
+ .child("ADI Backend"),
|
|
| 40 |
+ ) |
|
| 41 |
+ .when_some(backend, |this, backend| {
|
|
| 42 |
+ this.child(settings_backend_combo_with_popover( |
|
| 43 |
+ settings_adi_backend_combo_button(backend, adi_backend_picker_open) |
|
| 44 |
+ .id("adi-backend-combobox")
|
|
| 45 |
+ .when(backend_controls_disabled, |this| this.opacity(0.55)) |
|
| 46 |
+ .when(!backend_controls_disabled, |this| {
|
|
| 47 |
+ this.on_click(cx.listener(SettingsWindow::toggle_adi_backend_picker)) |
|
| 48 |
+ }), |
|
| 49 |
+ adi_backend_picker_open, |
|
| 50 |
+ dropdown_list((0..backends.len()).map(|index| {
|
|
| 51 |
+ settings_adi_backend_option(&backends[index], index == selected) |
|
| 52 |
+ .id(("adi-backend-option", index))
|
|
| 53 |
+ .on_click(cx.listener(move |this, event, window, cx| {
|
|
| 54 |
+ this.select_adi_backend(index, event, window, cx) |
|
| 55 |
+ })) |
|
| 56 |
+ })) |
|
| 57 |
+ .id("adi-backend-options"),
|
|
| 58 |
+ )) |
|
| 59 |
+ }), |
|
| 60 |
+ ); |
|
| 61 |
+ |
|
| 62 |
+ if let Some(backend) = backend {
|
|
| 63 |
+ content = content.child(adi_backend_status_section( |
|
| 64 |
+ backend, |
|
| 65 |
+ adi_operation, |
|
| 66 |
+ spinner_turns, |
|
| 67 |
+ cx, |
|
| 68 |
+ )); |
|
| 69 |
+ } |
|
| 70 |
+ |
|
| 71 |
+ content = content.child( |
|
| 72 |
+ machine_identity_section( |
|
| 73 |
+ displayed_machine_identity, |
|
| 74 |
+ editable_identity && !backend_controls_disabled, |
|
| 75 |
+ machine_identity_edit, |
|
| 76 |
+ cx, |
|
| 77 |
+ ) |
|
| 78 |
+ .when(backend_controls_disabled, |this| this.opacity(0.55)), |
|
| 79 |
+ ); |
|
| 80 |
+ |
|
| 81 |
+ settings_window_shell() |
|
| 82 |
+ .track_focus(focus_handle) |
|
| 83 |
+ .gap_4() |
|
| 84 |
+ .child(settings_window_header_with_action( |
|
| 85 |
+ settings_window_header( |
|
| 86 |
+ "Settings", |
|
| 87 |
+ "Choose the ADI backend and review the machine identity it uses.", |
|
| 88 |
+ ), |
|
| 89 |
+ open_data_folder_button(cx), |
|
| 90 |
+ )) |
|
| 91 |
+ .child(scroll_panel("adi-settings-scroll", scroll_handle, content))
|
|
| 92 |
+} |
|
| 93 |
+ |
|
| 94 |
+fn settings_backend_combo_with_popover( |
|
| 95 |
+ trigger: impl IntoElement, |
|
| 96 |
+ open: bool, |
|
| 97 |
+ popover: impl IntoElement, |
|
| 98 |
+) -> gpui::Div {
|
|
| 99 |
+ div().relative().child(trigger).when(open, |this| {
|
|
| 100 |
+ this.child( |
|
| 101 |
+ div().absolute().top_0().left_0().w(px(0.)).h(px(0.)).child( |
|
| 102 |
+ deferred( |
|
| 103 |
+ anchored() |
|
| 104 |
+ .snap_to_window_with_margin(px(8.)) |
|
| 105 |
+ .position_mode(AnchoredPositionMode::Local) |
|
| 106 |
+ .position(point(px(0.), px(ADI_BACKEND_COMBO_HEIGHT + 8.))) |
|
| 107 |
+ .child( |
|
| 108 |
+ div() |
|
| 109 |
+ .w(px(ADI_BACKEND_POPOVER_WIDTH)) |
|
| 110 |
+ .max_h(px(ADI_BACKEND_POPOVER_MAX_HEIGHT)) |
|
| 111 |
+ .rounded_md() |
|
| 112 |
+ .border_1() |
|
| 113 |
+ .border_color(rgb(0xd8e0df)) |
|
| 114 |
+ .bg(rgb(0xf8fbfa)) |
|
| 115 |
+ .id("adi-backend-popover-scroll")
|
|
| 116 |
+ .overflow_y_scroll() |
|
| 117 |
+ .scrollbar_width(px(8.)) |
|
| 118 |
+ .occlude() |
|
| 119 |
+ .child(popover), |
|
| 120 |
+ ), |
|
| 121 |
+ ) |
|
| 122 |
+ .with_priority(10), |
|
| 123 |
+ ), |
|
| 124 |
+ ) |
|
| 125 |
+ }) |
|
| 126 |
+} |
|
| 127 |
+ |
|
| 128 |
+fn settings_adi_backend_combo_button(backend: &AdiBackendOption, open: bool) -> gpui::Div {
|
|
| 129 |
+ let status = backend.availability.label(); |
|
| 130 |
+ |
|
| 131 |
+ div() |
|
| 132 |
+ .h(px(ADI_BACKEND_COMBO_HEIGHT)) |
|
| 133 |
+ .p_3() |
|
| 134 |
+ .rounded_md() |
|
| 135 |
+ .border_1() |
|
| 136 |
+ .border_color(if open { rgb(0x0f6f7a) } else { rgb(0xcfd8d6) })
|
|
| 137 |
+ .bg(rgb(0xffffff)) |
|
| 138 |
+ .cursor_pointer() |
|
| 139 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 140 |
+ .flex() |
|
| 141 |
+ .items_center() |
|
| 142 |
+ .gap_3() |
|
| 143 |
+ .child(status_dot(&status)) |
|
| 144 |
+ .child( |
|
| 145 |
+ div() |
|
| 146 |
+ .min_w_0() |
|
| 147 |
+ .flex() |
|
| 148 |
+ .flex_1() |
|
| 149 |
+ .flex_col() |
|
| 150 |
+ .gap_1() |
|
| 151 |
+ .child( |
|
| 152 |
+ div() |
|
| 153 |
+ .text_sm() |
|
| 154 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 155 |
+ .text_color(rgb(0x24333a)) |
|
| 156 |
+ .text_ellipsis() |
|
| 157 |
+ .child(backend.name.clone()), |
|
| 158 |
+ ) |
|
| 159 |
+ .child( |
|
| 160 |
+ div() |
|
| 161 |
+ .text_xs() |
|
| 162 |
+ .text_color(rgb(0x6a7a81)) |
|
| 163 |
+ .text_ellipsis() |
|
| 164 |
+ .child(backend.detail.clone()), |
|
| 165 |
+ ), |
|
| 166 |
+ ) |
|
| 167 |
+ .child(status_badge(status, false)) |
|
| 168 |
+ .child(chevron(open)) |
|
| 169 |
+} |
|
| 170 |
+ |
|
| 171 |
+fn settings_adi_backend_option(backend: &AdiBackendOption, selected: bool) -> gpui::Div {
|
|
| 172 |
+ let status = backend.availability.label(); |
|
| 173 |
+ |
|
| 174 |
+ div() |
|
| 175 |
+ .min_h(px(56.)) |
|
| 176 |
+ .p_2() |
|
| 177 |
+ .rounded_md() |
|
| 178 |
+ .border_1() |
|
| 179 |
+ .border_color(if selected {
|
|
| 180 |
+ rgb(0x0f6f7a) |
|
| 181 |
+ } else {
|
|
| 182 |
+ rgb(0xd8e0df) |
|
| 183 |
+ }) |
|
| 184 |
+ .bg(if selected {
|
|
| 185 |
+ rgb(0xf0fbfa) |
|
| 186 |
+ } else {
|
|
| 187 |
+ rgb(0xffffff) |
|
| 188 |
+ }) |
|
| 189 |
+ .cursor_pointer() |
|
| 190 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 191 |
+ .flex() |
|
| 192 |
+ .items_center() |
|
| 193 |
+ .gap_2() |
|
| 194 |
+ .child(status_dot(&status)) |
|
| 195 |
+ .child( |
|
| 196 |
+ div() |
|
| 197 |
+ .min_w_0() |
|
| 198 |
+ .flex() |
|
| 199 |
+ .flex_1() |
|
| 200 |
+ .flex_col() |
|
| 201 |
+ .gap_0p5() |
|
| 202 |
+ .child( |
|
| 203 |
+ div() |
|
| 204 |
+ .text_sm() |
|
| 205 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 206 |
+ .text_color(rgb(0x24333a)) |
|
| 207 |
+ .text_ellipsis() |
|
| 208 |
+ .child(backend.name.clone()), |
|
| 209 |
+ ) |
|
| 210 |
+ .child( |
|
| 211 |
+ div() |
|
| 212 |
+ .text_xs() |
|
| 213 |
+ .text_color(rgb(0x6a7a81)) |
|
| 214 |
+ .text_ellipsis() |
|
| 215 |
+ .child(backend.detail.clone()), |
|
| 216 |
+ ), |
|
| 217 |
+ ) |
|
| 218 |
+ .child(status_badge(status, selected)) |
|
| 219 |
+} |
|
| 220 |
+ |
|
| 221 |
+fn adi_backend_status_section( |
|
| 222 |
+ backend: &AdiBackendOption, |
|
| 223 |
+ adi_operation: Option<&AdiOperationState>, |
|
| 224 |
+ spinner_turns: f32, |
|
| 225 |
+ cx: &mut Context<SettingsWindow>, |
|
| 226 |
+) -> gpui::Div {
|
|
| 227 |
+ let status = backend.availability.label(); |
|
| 228 |
+ let color = backend_status_color(&status); |
|
| 229 |
+ let coreadi_operation = adi_operation.filter(|operation| operation.is_coreadi_install()); |
|
| 230 |
+ let provisioning_operation = adi_operation.filter(|operation| operation.is_provisioning()); |
|
| 231 |
+ let provisioning_label = if provisioning_operation.is_some() {
|
|
| 232 |
+ "Provisioning".into() |
|
| 233 |
+ } else {
|
|
| 234 |
+ backend.provisioning_state.label() |
|
| 235 |
+ }; |
|
| 236 |
+ |
|
| 237 |
+ div() |
|
| 238 |
+ .p_3() |
|
| 239 |
+ .rounded_md() |
|
| 240 |
+ .border_1() |
|
| 241 |
+ .border_color(rgb(0xd8e0df)) |
|
| 242 |
+ .bg(rgb(0xffffff)) |
|
| 243 |
+ .flex() |
|
| 244 |
+ .items_start() |
|
| 245 |
+ .gap_3() |
|
| 246 |
+ .child( |
|
| 247 |
+ div() |
|
| 248 |
+ .flex_none() |
|
| 249 |
+ .w(px(4.)) |
|
| 250 |
+ .h_full() |
|
| 251 |
+ .min_h(px(76.)) |
|
| 252 |
+ .rounded_full() |
|
| 253 |
+ .bg(rgb(color)), |
|
| 254 |
+ ) |
|
| 255 |
+ .child( |
|
| 256 |
+ div() |
|
| 257 |
+ .min_w_0() |
|
| 258 |
+ .flex() |
|
| 259 |
+ .flex_1() |
|
| 260 |
+ .flex_col() |
|
| 261 |
+ .gap_2() |
|
| 262 |
+ .child( |
|
| 263 |
+ div() |
|
| 264 |
+ .min_w_0() |
|
| 265 |
+ .flex() |
|
| 266 |
+ .flex_wrap() |
|
| 267 |
+ .items_center() |
|
| 268 |
+ .justify_between() |
|
| 269 |
+ .gap_2() |
|
| 270 |
+ .child( |
|
| 271 |
+ div() |
|
| 272 |
+ .min_w_0() |
|
| 273 |
+ .flex() |
|
| 274 |
+ .items_center() |
|
| 275 |
+ .gap_2() |
|
| 276 |
+ .child(status_dot(&status)) |
|
| 277 |
+ .child( |
|
| 278 |
+ div() |
|
| 279 |
+ .text_sm() |
|
| 280 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 281 |
+ .text_color(rgb(0x24333a)) |
|
| 282 |
+ .child("Status"),
|
|
| 283 |
+ ), |
|
| 284 |
+ ) |
|
| 285 |
+ .child(status_badge(status, false)), |
|
| 286 |
+ ) |
|
| 287 |
+ .child( |
|
| 288 |
+ div() |
|
| 289 |
+ .flex() |
|
| 290 |
+ .items_center() |
|
| 291 |
+ .gap_2() |
|
| 292 |
+ .child(settings_label("Provisioning"))
|
|
| 293 |
+ .child(status_badge(provisioning_label, false)), |
|
| 294 |
+ ) |
|
| 295 |
+ .when_some(backend.provisioning_state.detail(), |this, detail| {
|
|
| 296 |
+ this.child( |
|
| 297 |
+ div() |
|
| 298 |
+ .min_w_0() |
|
| 299 |
+ .text_xs() |
|
| 300 |
+ .line_height(px(17.)) |
|
| 301 |
+ .text_color(rgb(0x8a4a46)) |
|
| 302 |
+ .whitespace_normal() |
|
| 303 |
+ .child(detail), |
|
| 304 |
+ ) |
|
| 305 |
+ }) |
|
| 306 |
+ .children(backend.details.iter().map(|detail| {
|
|
| 307 |
+ adi_backend_detail_row(detail.label.clone(), detail.value.clone()) |
|
| 308 |
+ })) |
|
| 309 |
+ .when( |
|
| 310 |
+ matches!( |
|
| 311 |
+ &backend.provisioning_state, |
|
| 312 |
+ AdiProvisioningState::Provisioned |
|
| 313 |
+ ), |
|
| 314 |
+ |this| {
|
|
| 315 |
+ this.child(adi_status_button( |
|
| 316 |
+ "erase-adi-provisioning", |
|
| 317 |
+ "Erase Provisioning", |
|
| 318 |
+ false, |
|
| 319 |
+ 0xf4e9e7, |
|
| 320 |
+ 0x7d3430, |
|
| 321 |
+ cx.listener(SettingsWindow::erase_adi_backend_provisioning), |
|
| 322 |
+ )) |
|
| 323 |
+ }, |
|
| 324 |
+ ) |
|
| 325 |
+ .when( |
|
| 326 |
+ matches!( |
|
| 327 |
+ &backend.provisioning_state, |
|
| 328 |
+ AdiProvisioningState::NotProvisioned | AdiProvisioningState::Error(_) |
|
| 329 |
+ ), |
|
| 330 |
+ |this| {
|
|
| 331 |
+ this.child(adi_provision_button( |
|
| 332 |
+ provisioning_operation, |
|
| 333 |
+ spinner_turns, |
|
| 334 |
+ cx, |
|
| 335 |
+ )) |
|
| 336 |
+ }, |
|
| 337 |
+ ) |
|
| 338 |
+ .when(backend.repair_action.is_some(), |this| {
|
|
| 339 |
+ let label = backend |
|
| 340 |
+ .repair_action |
|
| 341 |
+ .map(|action| action.label()) |
|
| 342 |
+ .unwrap_or_else(|| "Fix Issues".into()); |
|
| 343 |
+ this.child(adi_repair_button( |
|
| 344 |
+ label, |
|
| 345 |
+ color, |
|
| 346 |
+ coreadi_operation, |
|
| 347 |
+ spinner_turns, |
|
| 348 |
+ cx, |
|
| 349 |
+ )) |
|
| 350 |
+ .when(backend.kind == AdiBackendKind::AndroidCoreAdi, |this| {
|
|
| 351 |
+ this.child(android_coreadi_apk_link(adi_operation.is_some(), cx)) |
|
| 352 |
+ }) |
|
| 353 |
+ }), |
|
| 354 |
+ ) |
|
| 355 |
+} |
|
| 356 |
+ |
|
| 357 |
+fn adi_provision_button( |
|
| 358 |
+ provisioning_operation: Option<&AdiOperationState>, |
|
| 359 |
+ spinner_turns: f32, |
|
| 360 |
+ cx: &mut Context<SettingsWindow>, |
|
| 361 |
+) -> impl IntoElement {
|
|
| 362 |
+ let provisioning = provisioning_operation.is_some(); |
|
| 363 |
+ let progress = provisioning_operation |
|
| 364 |
+ .map(AdiOperationState::progress) |
|
| 365 |
+ .unwrap_or(0.); |
|
| 366 |
+ let turn = if provisioning_operation.is_some_and(AdiOperationState::is_indeterminate) {
|
|
| 367 |
+ spinner_turns |
|
| 368 |
+ } else {
|
|
| 369 |
+ 0. |
|
| 370 |
+ }; |
|
| 371 |
+ let label = provisioning_operation |
|
| 372 |
+ .map(AdiOperationState::label) |
|
| 373 |
+ .unwrap_or_else(|| "Provision device".into()); |
|
| 374 |
+ |
|
| 375 |
+ div() |
|
| 376 |
+ .id("provision-adi")
|
|
| 377 |
+ .h_8() |
|
| 378 |
+ .px_3() |
|
| 379 |
+ .rounded_md() |
|
| 380 |
+ .bg(rgb(0x173f45)) |
|
| 381 |
+ .text_color(rgb(0xffffff)) |
|
| 382 |
+ .text_xs() |
|
| 383 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 384 |
+ .flex() |
|
| 385 |
+ .items_center() |
|
| 386 |
+ .justify_center() |
|
| 387 |
+ .gap_2() |
|
| 388 |
+ .when(!provisioning, |this| {
|
|
| 389 |
+ this.cursor_pointer() |
|
| 390 |
+ .hover(|style| style.opacity(0.88)) |
|
| 391 |
+ .on_click(cx.listener(SettingsWindow::provision_adi_backend)) |
|
| 392 |
+ }) |
|
| 393 |
+ .when_some(provisioning_operation, |this, _| {
|
|
| 394 |
+ this.child(progress_circle(progress, turn)) |
|
| 395 |
+ }) |
|
| 396 |
+ .child(label) |
|
| 397 |
+} |
|
| 398 |
+ |
|
| 399 |
+fn adi_repair_button( |
|
| 400 |
+ label: String, |
|
| 401 |
+ color: u32, |
|
| 402 |
+ coreadi_install: Option<&AdiOperationState>, |
|
| 403 |
+ spinner_turns: f32, |
|
| 404 |
+ cx: &mut Context<SettingsWindow>, |
|
| 405 |
+) -> impl IntoElement {
|
|
| 406 |
+ let installing = coreadi_install.is_some(); |
|
| 407 |
+ let progress_label = coreadi_install |
|
| 408 |
+ .map(AdiOperationState::label) |
|
| 409 |
+ .unwrap_or_else(|| label.into()); |
|
| 410 |
+ let progress = coreadi_install |
|
| 411 |
+ .map(AdiOperationState::progress) |
|
| 412 |
+ .unwrap_or(0.); |
|
| 413 |
+ let turn = if coreadi_install.is_some_and(AdiOperationState::is_indeterminate) {
|
|
| 414 |
+ spinner_turns |
|
| 415 |
+ } else {
|
|
| 416 |
+ 0. |
|
| 417 |
+ }; |
|
| 418 |
+ |
|
| 419 |
+ div() |
|
| 420 |
+ .id("repair-adi-backend")
|
|
| 421 |
+ .h_8() |
|
| 422 |
+ .px_3() |
|
| 423 |
+ .rounded_md() |
|
| 424 |
+ .bg(rgb(color)) |
|
| 425 |
+ .text_color(rgb(0xffffff)) |
|
| 426 |
+ .text_xs() |
|
| 427 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 428 |
+ .flex() |
|
| 429 |
+ .items_center() |
|
| 430 |
+ .justify_center() |
|
| 431 |
+ .gap_2() |
|
| 432 |
+ .when(!installing, |this| {
|
|
| 433 |
+ this.cursor_pointer() |
|
| 434 |
+ .hover(|style| style.opacity(0.88)) |
|
| 435 |
+ .on_click(cx.listener(SettingsWindow::repair_adi_backend)) |
|
| 436 |
+ }) |
|
| 437 |
+ .when_some(coreadi_install, |this, _| {
|
|
| 438 |
+ this.child(progress_circle(progress, turn)) |
|
| 439 |
+ }) |
|
| 440 |
+ .child(progress_label) |
|
| 441 |
+} |
|
| 442 |
+ |
|
| 443 |
+fn adi_backend_detail_row(label: String, value: String) -> gpui::Div {
|
|
| 444 |
+ div() |
|
| 445 |
+ .min_w_0() |
|
| 446 |
+ .flex() |
|
| 447 |
+ .items_center() |
|
| 448 |
+ .gap_2() |
|
| 449 |
+ .child( |
|
| 450 |
+ div() |
|
| 451 |
+ .w(px(112.)) |
|
| 452 |
+ .flex_none() |
|
| 453 |
+ .text_xs() |
|
| 454 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 455 |
+ .text_color(rgb(0x66767c)) |
|
| 456 |
+ .child(label), |
|
| 457 |
+ ) |
|
| 458 |
+ .child( |
|
| 459 |
+ div() |
|
| 460 |
+ .min_w_0() |
|
| 461 |
+ .flex_1() |
|
| 462 |
+ .text_xs() |
|
| 463 |
+ .line_height(px(17.)) |
|
| 464 |
+ .text_color(rgb(0x53666d)) |
|
| 465 |
+ .text_ellipsis() |
|
| 466 |
+ .child(value), |
|
| 467 |
+ ) |
|
| 468 |
+} |
|
| 469 |
+ |
|
| 470 |
+fn adi_status_button( |
|
| 471 |
+ id: &'static str, |
|
| 472 |
+ label: &'static str, |
|
| 473 |
+ disabled: bool, |
|
| 474 |
+ bg: u32, |
|
| 475 |
+ color: u32, |
|
| 476 |
+ listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, |
|
| 477 |
+) -> impl IntoElement {
|
|
| 478 |
+ div() |
|
| 479 |
+ .id(id) |
|
| 480 |
+ .h_8() |
|
| 481 |
+ .px_3() |
|
| 482 |
+ .rounded_md() |
|
| 483 |
+ .bg(rgb(bg)) |
|
| 484 |
+ .text_color(rgb(color)) |
|
| 485 |
+ .text_xs() |
|
| 486 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 487 |
+ .flex() |
|
| 488 |
+ .items_center() |
|
| 489 |
+ .justify_center() |
|
| 490 |
+ .when(disabled, |this| this.opacity(0.45)) |
|
| 491 |
+ .when(!disabled, |this| {
|
|
| 492 |
+ this.cursor_pointer() |
|
| 493 |
+ .hover(|style| style.opacity(0.88)) |
|
| 494 |
+ .on_click(listener) |
|
| 495 |
+ }) |
|
| 496 |
+ .child(label) |
|
| 497 |
+} |
|
| 498 |
+ |
|
| 499 |
+fn android_coreadi_apk_link(disabled: bool, cx: &mut Context<SettingsWindow>) -> gpui::Div {
|
|
| 500 |
+ div() |
|
| 501 |
+ .w_full() |
|
| 502 |
+ .flex() |
|
| 503 |
+ .items_center() |
|
| 504 |
+ .justify_center() |
|
| 505 |
+ .gap_1() |
|
| 506 |
+ .text_xs() |
|
| 507 |
+ .text_color(rgb(0x6a7a81)) |
|
| 508 |
+ .child("or")
|
|
| 509 |
+ .child( |
|
| 510 |
+ div() |
|
| 511 |
+ .id("select-coreadi-apk")
|
|
| 512 |
+ .text_color(rgb(0x0f6f7a)) |
|
| 513 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 514 |
+ .when(disabled, |this| this.opacity(0.45)) |
|
| 515 |
+ .when(!disabled, |this| {
|
|
| 516 |
+ this.cursor_pointer() |
|
| 517 |
+ .hover(|style| style.text_color(rgb(0x173f45))) |
|
| 518 |
+ .on_click(cx.listener(SettingsWindow::select_coreadi_apk)) |
|
| 519 |
+ }) |
|
| 520 |
+ .child("Select APK from the disk..."),
|
|
| 521 |
+ ) |
|
| 522 |
+} |
|
| 523 |
+ |
|
| 524 |
+fn machine_identity_section( |
|
| 525 |
+ identity: &MachineIdentity, |
|
| 526 |
+ editable: bool, |
|
| 527 |
+ editing: Option<&MachineIdentityEdit>, |
|
| 528 |
+ cx: &mut Context<SettingsWindow>, |
|
| 529 |
+) -> gpui::Div {
|
|
| 530 |
+ div() |
|
| 531 |
+ .p_3() |
|
| 532 |
+ .rounded_md() |
|
| 533 |
+ .border_1() |
|
| 534 |
+ .border_color(rgb(0xd8e0df)) |
|
| 535 |
+ .bg(rgb(0xffffff)) |
|
| 536 |
+ .flex() |
|
| 537 |
+ .flex_col() |
|
| 538 |
+ .gap_3() |
|
| 539 |
+ .child( |
|
| 540 |
+ div() |
|
| 541 |
+ .flex() |
|
| 542 |
+ .items_center() |
|
| 543 |
+ .gap_2() |
|
| 544 |
+ .child( |
|
| 545 |
+ div() |
|
| 546 |
+ .text_sm() |
|
| 547 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 548 |
+ .text_color(rgb(0x24333a)) |
|
| 549 |
+ .child("Machine Identity"),
|
|
| 550 |
+ ) |
|
| 551 |
+ .when(!editable, |this| this.child(read_only_badge())), |
|
| 552 |
+ ) |
|
| 553 |
+ .child(machine_identity_row( |
|
| 554 |
+ MachineIdentityField::MachineModel, |
|
| 555 |
+ identity.machine_name.clone(), |
|
| 556 |
+ editable, |
|
| 557 |
+ editing, |
|
| 558 |
+ cx, |
|
| 559 |
+ )) |
|
| 560 |
+ .child(machine_identity_row( |
|
| 561 |
+ MachineIdentityField::OsName, |
|
| 562 |
+ identity.os_name.clone(), |
|
| 563 |
+ editable, |
|
| 564 |
+ editing, |
|
| 565 |
+ cx, |
|
| 566 |
+ )) |
|
| 567 |
+ .child(machine_identity_row( |
|
| 568 |
+ MachineIdentityField::OsVersion, |
|
| 569 |
+ identity.os_version.clone(), |
|
| 570 |
+ editable, |
|
| 571 |
+ editing, |
|
| 572 |
+ cx, |
|
| 573 |
+ )) |
|
| 574 |
+ .child(machine_identity_row( |
|
| 575 |
+ MachineIdentityField::MachineId, |
|
| 576 |
+ identity.machine_id.clone(), |
|
| 577 |
+ editable, |
|
| 578 |
+ editing, |
|
| 579 |
+ cx, |
|
| 580 |
+ )) |
|
| 581 |
+} |
|
| 582 |
+ |
|
| 583 |
+fn machine_identity_row( |
|
| 584 |
+ field: MachineIdentityField, |
|
| 585 |
+ value: SharedString, |
|
| 586 |
+ editable: bool, |
|
| 587 |
+ editing: Option<&MachineIdentityEdit>, |
|
| 588 |
+ cx: &mut Context<SettingsWindow>, |
|
| 589 |
+) -> gpui::Div {
|
|
| 590 |
+ let label = field.label(); |
|
| 591 |
+ let is_editing = editing.is_some_and(|edit| edit.field == field); |
|
| 592 |
+ |
|
| 593 |
+ div() |
|
| 594 |
+ .min_w_0() |
|
| 595 |
+ .flex() |
|
| 596 |
+ .items_center() |
|
| 597 |
+ .gap_3() |
|
| 598 |
+ .child( |
|
| 599 |
+ div() |
|
| 600 |
+ .w(px(104.)) |
|
| 601 |
+ .flex_none() |
|
| 602 |
+ .text_xs() |
|
| 603 |
+ .text_color(rgb(0x7b8a90)) |
|
| 604 |
+ .child(label), |
|
| 605 |
+ ) |
|
| 606 |
+ .when_some(editing.filter(|edit| edit.field == field), |this, edit| {
|
|
| 607 |
+ this.child(machine_identity_editor(&edit.input)) |
|
| 608 |
+ .child(machine_identity_icon_button( |
|
| 609 |
+ ("save-machine-identity", machine_identity_id(field)),
|
|
| 610 |
+ "icons/check.svg", |
|
| 611 |
+ cx.listener(SettingsWindow::save_machine_identity_edit_from_button), |
|
| 612 |
+ )) |
|
| 613 |
+ .child(machine_identity_icon_button( |
|
| 614 |
+ ("cancel-machine-identity", machine_identity_id(field)),
|
|
| 615 |
+ "icons/x.svg", |
|
| 616 |
+ cx.listener(SettingsWindow::cancel_machine_identity_edit), |
|
| 617 |
+ )) |
|
| 618 |
+ }) |
|
| 619 |
+ .when(!is_editing, |this| {
|
|
| 620 |
+ this.child( |
|
| 621 |
+ div() |
|
| 622 |
+ .min_w_0() |
|
| 623 |
+ .flex_1() |
|
| 624 |
+ .text_sm() |
|
| 625 |
+ .text_color(rgb(0x405057)) |
|
| 626 |
+ .text_ellipsis() |
|
| 627 |
+ .child(value), |
|
| 628 |
+ ) |
|
| 629 |
+ .when(editable, |this| {
|
|
| 630 |
+ this.child( |
|
| 631 |
+ div() |
|
| 632 |
+ .id(("edit-machine-identity", machine_identity_id(field)))
|
|
| 633 |
+ .flex_none() |
|
| 634 |
+ .w_6() |
|
| 635 |
+ .h_6() |
|
| 636 |
+ .rounded_md() |
|
| 637 |
+ .bg(rgb(0xebf1f0)) |
|
| 638 |
+ .text_color(rgb(0x53666d)) |
|
| 639 |
+ .flex() |
|
| 640 |
+ .items_center() |
|
| 641 |
+ .justify_center() |
|
| 642 |
+ .cursor_pointer() |
|
| 643 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 644 |
+ .on_click(cx.listener(move |settings, event, window, cx| {
|
|
| 645 |
+ settings.begin_machine_identity_edit(field, event, window, cx) |
|
| 646 |
+ })) |
|
| 647 |
+ .child(lucide_icon("icons/pencil.svg")),
|
|
| 648 |
+ ) |
|
| 649 |
+ }) |
|
| 650 |
+ }) |
|
| 651 |
+} |
|
| 652 |
+ |
|
| 653 |
+fn machine_identity_icon_button( |
|
| 654 |
+ id: (&'static str, usize), |
|
| 655 |
+ icon: &'static str, |
|
| 656 |
+ listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, |
|
| 657 |
+) -> impl IntoElement {
|
|
| 658 |
+ div() |
|
| 659 |
+ .id(id) |
|
| 660 |
+ .flex_none() |
|
| 661 |
+ .w_6() |
|
| 662 |
+ .h_6() |
|
| 663 |
+ .rounded_md() |
|
| 664 |
+ .bg(rgb(0xebf1f0)) |
|
| 665 |
+ .text_color(rgb(0x53666d)) |
|
| 666 |
+ .flex() |
|
| 667 |
+ .items_center() |
|
| 668 |
+ .justify_center() |
|
| 669 |
+ .cursor_pointer() |
|
| 670 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 671 |
+ .on_click(listener) |
|
| 672 |
+ .child(lucide_icon(icon)) |
|
| 673 |
+} |
|
| 674 |
+ |
|
| 675 |
+fn machine_identity_id(field: MachineIdentityField) -> usize {
|
|
| 676 |
+ match field {
|
|
| 677 |
+ MachineIdentityField::MachineModel => 0, |
|
| 678 |
+ MachineIdentityField::OsName => 1, |
|
| 679 |
+ MachineIdentityField::OsVersion => 2, |
|
| 680 |
+ MachineIdentityField::MachineId => 3, |
|
| 681 |
+ } |
|
| 682 |
+} |
|
| 683 |
+ |
|
| 684 |
+fn status_badge(label: String, selected: bool) -> gpui::Div {
|
|
| 685 |
+ let color = backend_status_color(&label); |
|
| 686 |
+ |
|
| 687 |
+ div() |
|
| 688 |
+ .flex_none() |
|
| 689 |
+ .px_2() |
|
| 690 |
+ .h_6() |
|
| 691 |
+ .rounded_full() |
|
| 692 |
+ .bg(if selected {
|
|
| 693 |
+ rgb(status_badge_selected_bg(&label)) |
|
| 694 |
+ } else {
|
|
| 695 |
+ rgb(status_badge_bg(&label)) |
|
| 696 |
+ }) |
|
| 697 |
+ .text_color(rgb(color)) |
|
| 698 |
+ .text_xs() |
|
| 699 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 700 |
+ .flex() |
|
| 701 |
+ .items_center() |
|
| 702 |
+ .justify_center() |
|
| 703 |
+ .child(label) |
|
| 704 |
+} |
|
| 705 |
+ |
|
| 706 |
+fn status_dot(status: &str) -> gpui::Div {
|
|
| 707 |
+ div() |
|
| 708 |
+ .flex_none() |
|
| 709 |
+ .w_2() |
|
| 710 |
+ .h_2() |
|
| 711 |
+ .rounded_full() |
|
| 712 |
+ .bg(rgb(backend_status_color(status))) |
|
| 713 |
+} |
|
| 714 |
+ |
|
| 715 |
+fn backend_status_color(status: &str) -> u32 {
|
|
| 716 |
+ match status {
|
|
| 717 |
+ "Ready" => 0x1d6b45, |
|
| 718 |
+ "Needs setup" => 0x9a6a14, |
|
| 719 |
+ "Offline" => 0x9a302b, |
|
| 720 |
+ "Provisioned" => 0x1d6b45, |
|
| 721 |
+ "Provisioning" => 0x1d6b45, |
|
| 722 |
+ "Not provisioned" => 0x9a6a14, |
|
| 723 |
+ "Not available" => 0x53666d, |
|
| 724 |
+ "Failed" => 0x9a302b, |
|
| 725 |
+ _ => 0x53666d, |
|
| 726 |
+ } |
|
| 727 |
+} |
|
| 728 |
+ |
|
| 729 |
+fn status_badge_bg(status: &str) -> u32 {
|
|
| 730 |
+ match status {
|
|
| 731 |
+ "Ready" => 0xe5f3ec, |
|
| 732 |
+ "Needs setup" => 0xf6edda, |
|
| 733 |
+ "Offline" => 0xf4e4e2, |
|
| 734 |
+ "Provisioned" => 0xe5f3ec, |
|
| 735 |
+ "Provisioning" => 0xe5f3ec, |
|
| 736 |
+ "Not provisioned" => 0xf6edda, |
|
| 737 |
+ "Not available" => 0xebf1f0, |
|
| 738 |
+ "Failed" => 0xf4e4e2, |
|
| 739 |
+ _ => 0xebf1f0, |
|
| 740 |
+ } |
|
| 741 |
+} |
|
| 742 |
+ |
|
| 743 |
+fn status_badge_selected_bg(status: &str) -> u32 {
|
|
| 744 |
+ match status {
|
|
| 745 |
+ "Ready" => 0xd3eadc, |
|
| 746 |
+ "Needs setup" => 0xeadbb8, |
|
| 747 |
+ "Offline" => 0xebcecb, |
|
| 748 |
+ "Provisioned" => 0xd3eadc, |
|
| 749 |
+ "Provisioning" => 0xd3eadc, |
|
| 750 |
+ "Not provisioned" => 0xeadbb8, |
|
| 751 |
+ "Not available" => 0xdfe8e6, |
|
| 752 |
+ "Failed" => 0xebcecb, |
|
| 753 |
+ _ => 0xd7efec, |
|
| 754 |
+ } |
|
| 755 |
+} |
|
| 756 |
+ |
|
| 757 |
+fn read_only_badge() -> gpui::Div {
|
|
| 758 |
+ div() |
|
| 759 |
+ .px_2() |
|
| 760 |
+ .h_5() |
|
| 761 |
+ .rounded_full() |
|
| 762 |
+ .bg(rgb(0xf0f2f2)) |
|
| 763 |
+ .text_color(rgb(0x718086)) |
|
| 764 |
+ .text_xs() |
|
| 765 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 766 |
+ .flex() |
|
| 767 |
+ .items_center() |
|
| 768 |
+ .justify_center() |
|
| 769 |
+ .child("Read-only")
|
|
| 770 |
+} |
| ... | ... |
@@ -0,0 +1,929 @@ |
| 1 |
+use super::*; |
|
| 2 |
+ |
|
| 3 |
+pub(super) fn render( |
|
| 4 |
+ focus_handle: &FocusHandle, |
|
| 5 |
+ scroll_handle: &ScrollHandle, |
|
| 6 |
+ app: &AppOption, |
|
| 7 |
+ enabled_patches: &[bool], |
|
| 8 |
+ team_id: &SharedString, |
|
| 9 |
+ app_detail_edit: Option<&AppDetailEdit>, |
|
| 10 |
+ selected_entitlement: Option<usize>, |
|
| 11 |
+ entitlement_edit: Option<&EntitlementEdit>, |
|
| 12 |
+ entitlement_type_picker_open: bool, |
|
| 13 |
+ cx: &mut Context<SettingsWindow>, |
|
| 14 |
+) -> gpui::Div {
|
|
| 15 |
+ let patch_summary = enabled_patch_summary(app, enabled_patches); |
|
| 16 |
+ |
|
| 17 |
+ settings_window_shell() |
|
| 18 |
+ .track_focus(focus_handle) |
|
| 19 |
+ .capture_key_down(cx.listener(SettingsWindow::handle_app_settings_key)) |
|
| 20 |
+ .child(scroll_panel( |
|
| 21 |
+ "app-settings-scroll", |
|
| 22 |
+ scroll_handle, |
|
| 23 |
+ div() |
|
| 24 |
+ .flex() |
|
| 25 |
+ .flex_col() |
|
| 26 |
+ .gap_4() |
|
| 27 |
+ .child(settings_window_header( |
|
| 28 |
+ "App Settings", |
|
| 29 |
+ "Review metadata and entitlements before signing.", |
|
| 30 |
+ )) |
|
| 31 |
+ .child( |
|
| 32 |
+ div() |
|
| 33 |
+ .flex() |
|
| 34 |
+ .gap_3() |
|
| 35 |
+ .p_3() |
|
| 36 |
+ .rounded_md() |
|
| 37 |
+ .border_1() |
|
| 38 |
+ .border_color(rgb(0xd8e0df)) |
|
| 39 |
+ .bg(rgb(0xffffff)) |
|
| 40 |
+ .child(app_icon_editor(app, cx)) |
|
| 41 |
+ .child( |
|
| 42 |
+ div() |
|
| 43 |
+ .min_w_0() |
|
| 44 |
+ .flex() |
|
| 45 |
+ .flex_1() |
|
| 46 |
+ .flex_col() |
|
| 47 |
+ .gap_2() |
|
| 48 |
+ .child(app_detail_row( |
|
| 49 |
+ AppMetadataField::Name, |
|
| 50 |
+ app, |
|
| 51 |
+ true, |
|
| 52 |
+ app_detail_edit, |
|
| 53 |
+ cx, |
|
| 54 |
+ )) |
|
| 55 |
+ .child(app_detail_row( |
|
| 56 |
+ AppMetadataField::BundleId, |
|
| 57 |
+ app, |
|
| 58 |
+ true, |
|
| 59 |
+ app_detail_edit, |
|
| 60 |
+ cx, |
|
| 61 |
+ )) |
|
| 62 |
+ .child(app_detail_row( |
|
| 63 |
+ AppMetadataField::Version, |
|
| 64 |
+ app, |
|
| 65 |
+ true, |
|
| 66 |
+ app_detail_edit, |
|
| 67 |
+ cx, |
|
| 68 |
+ )) |
|
| 69 |
+ .child(app_detail_row( |
|
| 70 |
+ AppMetadataField::Build, |
|
| 71 |
+ app, |
|
| 72 |
+ true, |
|
| 73 |
+ app_detail_edit, |
|
| 74 |
+ cx, |
|
| 75 |
+ )), |
|
| 76 |
+ ), |
|
| 77 |
+ ) |
|
| 78 |
+ .child( |
|
| 79 |
+ div() |
|
| 80 |
+ .flex() |
|
| 81 |
+ .flex_col() |
|
| 82 |
+ .gap_2() |
|
| 83 |
+ .p_3() |
|
| 84 |
+ .rounded_md() |
|
| 85 |
+ .border_1() |
|
| 86 |
+ .border_color(rgb(0xd8e0df)) |
|
| 87 |
+ .bg(rgb(0xffffff)) |
|
| 88 |
+ .child(app_detail_row( |
|
| 89 |
+ AppMetadataField::Executable, |
|
| 90 |
+ app, |
|
| 91 |
+ true, |
|
| 92 |
+ app_detail_edit, |
|
| 93 |
+ cx, |
|
| 94 |
+ )) |
|
| 95 |
+ .child(app_detail_row( |
|
| 96 |
+ AppMetadataField::MinimumOs, |
|
| 97 |
+ app, |
|
| 98 |
+ true, |
|
| 99 |
+ app_detail_edit, |
|
| 100 |
+ cx, |
|
| 101 |
+ )) |
|
| 102 |
+ .child(app_detail_row( |
|
| 103 |
+ AppMetadataField::SupportedDevices, |
|
| 104 |
+ app, |
|
| 105 |
+ true, |
|
| 106 |
+ app_detail_edit, |
|
| 107 |
+ cx, |
|
| 108 |
+ )) |
|
| 109 |
+ .child(read_only_app_detail_row("IPA", app.path.to_string()))
|
|
| 110 |
+ .child(read_only_app_detail_row("Patches", patch_summary)),
|
|
| 111 |
+ ) |
|
| 112 |
+ .child(entitlements_table( |
|
| 113 |
+ app, |
|
| 114 |
+ team_id, |
|
| 115 |
+ selected_entitlement, |
|
| 116 |
+ entitlement_edit, |
|
| 117 |
+ entitlement_type_picker_open, |
|
| 118 |
+ cx, |
|
| 119 |
+ )), |
|
| 120 |
+ )) |
|
| 121 |
+} |
|
| 122 |
+ |
|
| 123 |
+fn app_icon_editor(app: &AppOption, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 124 |
+ let initial = app |
|
| 125 |
+ .name() |
|
| 126 |
+ .to_string() |
|
| 127 |
+ .chars() |
|
| 128 |
+ .next() |
|
| 129 |
+ .map(|c| c.to_string()) |
|
| 130 |
+ .unwrap_or_else(|| "A".to_string()); |
|
| 131 |
+ let icon_path = app.displayed_icon_path().map(|path| path.to_string()); |
|
| 132 |
+ |
|
| 133 |
+ div() |
|
| 134 |
+ .flex_none() |
|
| 135 |
+ .w(px(84.)) |
|
| 136 |
+ .h(px(84.)) |
|
| 137 |
+ .rounded_md() |
|
| 138 |
+ .border_1() |
|
| 139 |
+ .border_color(rgb(0xcfd8d6)) |
|
| 140 |
+ .bg(rgb(0x173f45)) |
|
| 141 |
+ .text_color(rgb(0xffffff)) |
|
| 142 |
+ .overflow_hidden() |
|
| 143 |
+ .cursor_pointer() |
|
| 144 |
+ .hover(|style| style.bg(rgb(0x20545c))) |
|
| 145 |
+ .flex() |
|
| 146 |
+ .flex_col() |
|
| 147 |
+ .items_center() |
|
| 148 |
+ .justify_center() |
|
| 149 |
+ .gap_1() |
|
| 150 |
+ .id("app-icon-editor")
|
|
| 151 |
+ .on_click(cx.listener(SettingsWindow::edit_app_icon)) |
|
| 152 |
+ .child(if let Some(icon_path) = icon_path {
|
|
| 153 |
+ div().size_full().overflow_hidden().child( |
|
| 154 |
+ img(Arc::<std::path::Path>::from(std::path::PathBuf::from( |
|
| 155 |
+ icon_path, |
|
| 156 |
+ ))) |
|
| 157 |
+ .size_full() |
|
| 158 |
+ .object_fit(ObjectFit::Cover), |
|
| 159 |
+ ) |
|
| 160 |
+ } else {
|
|
| 161 |
+ div() |
|
| 162 |
+ .flex() |
|
| 163 |
+ .flex_col() |
|
| 164 |
+ .items_center() |
|
| 165 |
+ .justify_center() |
|
| 166 |
+ .gap_1() |
|
| 167 |
+ .text_2xl() |
|
| 168 |
+ .font_weight(FontWeight::BOLD) |
|
| 169 |
+ .child(initial) |
|
| 170 |
+ .child(div().text_xs().child("Edit icon"))
|
|
| 171 |
+ }) |
|
| 172 |
+} |
|
| 173 |
+ |
|
| 174 |
+fn app_detail_row( |
|
| 175 |
+ field: AppMetadataField, |
|
| 176 |
+ app: &AppOption, |
|
| 177 |
+ editable: bool, |
|
| 178 |
+ edit: Option<&AppDetailEdit>, |
|
| 179 |
+ cx: &mut Context<SettingsWindow>, |
|
| 180 |
+) -> gpui::Div {
|
|
| 181 |
+ let is_editing = edit.is_some_and(|edit| edit.field() == field); |
|
| 182 |
+ let changed = app.field_is_overridden(field); |
|
| 183 |
+ |
|
| 184 |
+ div() |
|
| 185 |
+ .min_w_0() |
|
| 186 |
+ .flex() |
|
| 187 |
+ .items_center() |
|
| 188 |
+ .gap_3() |
|
| 189 |
+ .child( |
|
| 190 |
+ div() |
|
| 191 |
+ .w(px(96.)) |
|
| 192 |
+ .flex_none() |
|
| 193 |
+ .text_xs() |
|
| 194 |
+ .text_color(rgb(0x7b8a90)) |
|
| 195 |
+ .child(field.label()), |
|
| 196 |
+ ) |
|
| 197 |
+ .child( |
|
| 198 |
+ if let Some(AppDetailEdit::Text { input, .. }) =
|
|
| 199 |
+ edit.filter(|edit| edit.field() == field) |
|
| 200 |
+ {
|
|
| 201 |
+ machine_identity_editor(input) |
|
| 202 |
+ } else if let Some(AppDetailEdit::SupportedDevices { selected }) =
|
|
| 203 |
+ edit.filter(|edit| edit.field() == field) |
|
| 204 |
+ {
|
|
| 205 |
+ supported_devices_editor(selected, cx) |
|
| 206 |
+ } else {
|
|
| 207 |
+ div() |
|
| 208 |
+ .min_w_0() |
|
| 209 |
+ .flex_1() |
|
| 210 |
+ .text_sm() |
|
| 211 |
+ .text_color(rgb(if changed { 0x173f45 } else { 0x405057 }))
|
|
| 212 |
+ .text_ellipsis() |
|
| 213 |
+ .child(app.field_display_value(field)) |
|
| 214 |
+ }, |
|
| 215 |
+ ) |
|
| 216 |
+ .when(editable, |this| {
|
|
| 217 |
+ this.child(app_detail_controls(field, changed, is_editing, cx)) |
|
| 218 |
+ }) |
|
| 219 |
+} |
|
| 220 |
+ |
|
| 221 |
+fn supported_devices_editor( |
|
| 222 |
+ selected: &[SupportedDeviceFamily], |
|
| 223 |
+ cx: &mut Context<SettingsWindow>, |
|
| 224 |
+) -> gpui::Div {
|
|
| 225 |
+ div() |
|
| 226 |
+ .min_w_0() |
|
| 227 |
+ .flex_1() |
|
| 228 |
+ .flex() |
|
| 229 |
+ .flex_wrap() |
|
| 230 |
+ .gap_1() |
|
| 231 |
+ .children(SupportedDeviceFamily::ALL.into_iter().map(|family| {
|
|
| 232 |
+ let active = selected.contains(&family); |
|
| 233 |
+ div() |
|
| 234 |
+ .id(("supported-device-family", family as usize))
|
|
| 235 |
+ .px_2() |
|
| 236 |
+ .h_6() |
|
| 237 |
+ .rounded_md() |
|
| 238 |
+ .border_1() |
|
| 239 |
+ .border_color(if active { rgb(0x168291) } else { rgb(0xd8e0df) })
|
|
| 240 |
+ .bg(if active { rgb(0xf0fbfa) } else { rgb(0xffffff) })
|
|
| 241 |
+ .text_color(rgb(if active { 0x173f45 } else { 0x53666d }))
|
|
| 242 |
+ .text_xs() |
|
| 243 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 244 |
+ .flex() |
|
| 245 |
+ .items_center() |
|
| 246 |
+ .justify_center() |
|
| 247 |
+ .cursor_pointer() |
|
| 248 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 249 |
+ .on_click(cx.listener(move |settings, event, window, cx| {
|
|
| 250 |
+ settings.toggle_supported_device_family(family, event, window, cx) |
|
| 251 |
+ })) |
|
| 252 |
+ .child(family.label()) |
|
| 253 |
+ })) |
|
| 254 |
+} |
|
| 255 |
+ |
|
| 256 |
+fn read_only_app_detail_row(label: &'static str, value: String) -> gpui::Div {
|
|
| 257 |
+ div() |
|
| 258 |
+ .min_w_0() |
|
| 259 |
+ .flex() |
|
| 260 |
+ .items_center() |
|
| 261 |
+ .gap_3() |
|
| 262 |
+ .child( |
|
| 263 |
+ div() |
|
| 264 |
+ .w(px(96.)) |
|
| 265 |
+ .flex_none() |
|
| 266 |
+ .text_xs() |
|
| 267 |
+ .text_color(rgb(0x7b8a90)) |
|
| 268 |
+ .child(label), |
|
| 269 |
+ ) |
|
| 270 |
+ .child( |
|
| 271 |
+ div() |
|
| 272 |
+ .min_w_0() |
|
| 273 |
+ .flex_1() |
|
| 274 |
+ .text_sm() |
|
| 275 |
+ .text_color(rgb(0x405057)) |
|
| 276 |
+ .text_ellipsis() |
|
| 277 |
+ .child(value), |
|
| 278 |
+ ) |
|
| 279 |
+} |
|
| 280 |
+ |
|
| 281 |
+fn app_detail_controls( |
|
| 282 |
+ field: AppMetadataField, |
|
| 283 |
+ changed: bool, |
|
| 284 |
+ editing: bool, |
|
| 285 |
+ cx: &mut Context<SettingsWindow>, |
|
| 286 |
+) -> impl IntoElement {
|
|
| 287 |
+ div() |
|
| 288 |
+ .flex_none() |
|
| 289 |
+ .w(px(86.)) |
|
| 290 |
+ .flex() |
|
| 291 |
+ .items_center() |
|
| 292 |
+ .justify_end() |
|
| 293 |
+ .gap_1() |
|
| 294 |
+ .child(app_detail_revert_slot(field, changed, cx)) |
|
| 295 |
+ .when(editing, |this| {
|
|
| 296 |
+ this.child(confirm_app_detail_button(cx)) |
|
| 297 |
+ .child(cancel_app_detail_button(cx)) |
|
| 298 |
+ }) |
|
| 299 |
+ .when(!editing, |this| this.child(edit_button(field, cx))) |
|
| 300 |
+} |
|
| 301 |
+ |
|
| 302 |
+fn edit_button(field: AppMetadataField, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 303 |
+ div() |
|
| 304 |
+ .flex_none() |
|
| 305 |
+ .w_6() |
|
| 306 |
+ .h_6() |
|
| 307 |
+ .rounded_md() |
|
| 308 |
+ .bg(rgb(0xebf1f0)) |
|
| 309 |
+ .text_color(rgb(0x53666d)) |
|
| 310 |
+ .flex() |
|
| 311 |
+ .items_center() |
|
| 312 |
+ .justify_center() |
|
| 313 |
+ .cursor_pointer() |
|
| 314 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 315 |
+ .id(("edit-app-detail", app_detail_id(field)))
|
|
| 316 |
+ .on_click(cx.listener(move |settings, event, window, cx| {
|
|
| 317 |
+ settings.begin_app_detail_edit(field, event, window, cx) |
|
| 318 |
+ })) |
|
| 319 |
+ .child(lucide_icon("icons/pencil.svg"))
|
|
| 320 |
+} |
|
| 321 |
+ |
|
| 322 |
+fn confirm_app_detail_button(cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 323 |
+ div() |
|
| 324 |
+ .flex_none() |
|
| 325 |
+ .w_6() |
|
| 326 |
+ .h_6() |
|
| 327 |
+ .rounded_md() |
|
| 328 |
+ .bg(rgb(0xd3eadc)) |
|
| 329 |
+ .text_color(rgb(0x1d6b45)) |
|
| 330 |
+ .flex() |
|
| 331 |
+ .items_center() |
|
| 332 |
+ .justify_center() |
|
| 333 |
+ .cursor_pointer() |
|
| 334 |
+ .hover(|style| style.bg(rgb(0xc5e2d1))) |
|
| 335 |
+ .id("save-app-detail")
|
|
| 336 |
+ .on_click(cx.listener(SettingsWindow::save_app_detail_edit_from_button)) |
|
| 337 |
+ .child(lucide_icon("icons/check.svg"))
|
|
| 338 |
+} |
|
| 339 |
+ |
|
| 340 |
+fn cancel_app_detail_button(cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 341 |
+ div() |
|
| 342 |
+ .flex_none() |
|
| 343 |
+ .w_6() |
|
| 344 |
+ .h_6() |
|
| 345 |
+ .rounded_md() |
|
| 346 |
+ .bg(rgb(0xebcecb)) |
|
| 347 |
+ .text_color(rgb(0x9a302b)) |
|
| 348 |
+ .flex() |
|
| 349 |
+ .items_center() |
|
| 350 |
+ .justify_center() |
|
| 351 |
+ .cursor_pointer() |
|
| 352 |
+ .hover(|style| style.bg(rgb(0xe2c0bd))) |
|
| 353 |
+ .id("cancel-app-detail")
|
|
| 354 |
+ .on_click(cx.listener(SettingsWindow::cancel_app_detail_edit)) |
|
| 355 |
+ .child(lucide_icon("icons/x.svg"))
|
|
| 356 |
+} |
|
| 357 |
+ |
|
| 358 |
+fn app_detail_revert_slot( |
|
| 359 |
+ field: AppMetadataField, |
|
| 360 |
+ changed: bool, |
|
| 361 |
+ cx: &mut Context<SettingsWindow>, |
|
| 362 |
+) -> impl IntoElement {
|
|
| 363 |
+ div() |
|
| 364 |
+ .flex_none() |
|
| 365 |
+ .w(px(52.)) |
|
| 366 |
+ .h_6() |
|
| 367 |
+ .rounded_md() |
|
| 368 |
+ .bg(rgb(0xebf1f0)) |
|
| 369 |
+ .text_color(rgb(0x53666d)) |
|
| 370 |
+ .text_xs() |
|
| 371 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 372 |
+ .flex() |
|
| 373 |
+ .items_center() |
|
| 374 |
+ .justify_center() |
|
| 375 |
+ .id(("revert-app-detail", app_detail_id(field)))
|
|
| 376 |
+ .when(!changed, |this| this.opacity(0.)) |
|
| 377 |
+ .when(changed, |this| {
|
|
| 378 |
+ this.cursor_pointer() |
|
| 379 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 380 |
+ .on_click(cx.listener(move |settings, event, window, cx| {
|
|
| 381 |
+ settings.revert_app_detail_field(field, event, window, cx) |
|
| 382 |
+ })) |
|
| 383 |
+ }) |
|
| 384 |
+ .child("Revert")
|
|
| 385 |
+} |
|
| 386 |
+ |
|
| 387 |
+fn app_detail_id(field: AppMetadataField) -> usize {
|
|
| 388 |
+ match field {
|
|
| 389 |
+ AppMetadataField::Name => 0, |
|
| 390 |
+ AppMetadataField::BundleId => 1, |
|
| 391 |
+ AppMetadataField::Version => 2, |
|
| 392 |
+ AppMetadataField::Build => 3, |
|
| 393 |
+ AppMetadataField::Executable => 4, |
|
| 394 |
+ AppMetadataField::MinimumOs => 5, |
|
| 395 |
+ AppMetadataField::SupportedDevices => 6, |
|
| 396 |
+ } |
|
| 397 |
+} |
|
| 398 |
+ |
|
| 399 |
+fn enabled_patch_summary(app: &AppOption, enabled_patches: &[bool]) -> String {
|
|
| 400 |
+ let enabled = app |
|
| 401 |
+ .patches |
|
| 402 |
+ .iter() |
|
| 403 |
+ .enumerate() |
|
| 404 |
+ .filter_map(|(index, patch)| {
|
|
| 405 |
+ enabled_patches |
|
| 406 |
+ .get(index) |
|
| 407 |
+ .copied() |
|
| 408 |
+ .filter(|enabled| *enabled) |
|
| 409 |
+ .map(|_| format!("{} ({})", patch.name, patch.detail))
|
|
| 410 |
+ }) |
|
| 411 |
+ .collect::<Vec<_>>(); |
|
| 412 |
+ |
|
| 413 |
+ if enabled.is_empty() {
|
|
| 414 |
+ "No patch changes selected".to_string() |
|
| 415 |
+ } else {
|
|
| 416 |
+ enabled.join(", ")
|
|
| 417 |
+ } |
|
| 418 |
+} |
|
| 419 |
+ |
|
| 420 |
+fn entitlements_table( |
|
| 421 |
+ app: &AppOption, |
|
| 422 |
+ team_id: &SharedString, |
|
| 423 |
+ selected_entitlement: Option<usize>, |
|
| 424 |
+ edit: Option<&EntitlementEdit>, |
|
| 425 |
+ type_picker_open: bool, |
|
| 426 |
+ cx: &mut Context<SettingsWindow>, |
|
| 427 |
+) -> gpui::Div {
|
|
| 428 |
+ let changed = app.entitlements_are_overridden(); |
|
| 429 |
+ let entitlements = app.effective_entitlements(team_id); |
|
| 430 |
+ let default_entitlements = app.default_effective_entitlements(team_id); |
|
| 431 |
+ |
|
| 432 |
+ div() |
|
| 433 |
+ .flex() |
|
| 434 |
+ .flex_col() |
|
| 435 |
+ .rounded_md() |
|
| 436 |
+ .border_1() |
|
| 437 |
+ .border_color(rgb(0xd8e0df)) |
|
| 438 |
+ .bg(rgb(0xffffff)) |
|
| 439 |
+ .overflow_hidden() |
|
| 440 |
+ .child( |
|
| 441 |
+ div() |
|
| 442 |
+ .px_3() |
|
| 443 |
+ .py_2() |
|
| 444 |
+ .border_b_1() |
|
| 445 |
+ .border_color(rgb(0xd8e0df)) |
|
| 446 |
+ .flex() |
|
| 447 |
+ .items_center() |
|
| 448 |
+ .justify_start() |
|
| 449 |
+ .gap_2() |
|
| 450 |
+ .child( |
|
| 451 |
+ div() |
|
| 452 |
+ .text_sm() |
|
| 453 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 454 |
+ .text_color(rgb(0x24333a)) |
|
| 455 |
+ .child("Entitlements"),
|
|
| 456 |
+ ) |
|
| 457 |
+ .child(entitlements_actions(changed, cx)), |
|
| 458 |
+ ) |
|
| 459 |
+ .child(entitlement_header_row()) |
|
| 460 |
+ .children(entitlements.iter().enumerate().map(|(index, entitlement)| {
|
|
| 461 |
+ let row_changed = changed |
|
| 462 |
+ && entitlement_revert_target(entitlement, index, &default_entitlements).as_ref() |
|
| 463 |
+ != Some(entitlement); |
|
| 464 |
+ entitlement_row( |
|
| 465 |
+ index, |
|
| 466 |
+ entitlement, |
|
| 467 |
+ selected_entitlement == Some(index), |
|
| 468 |
+ row_changed, |
|
| 469 |
+ edit, |
|
| 470 |
+ type_picker_open, |
|
| 471 |
+ cx, |
|
| 472 |
+ ) |
|
| 473 |
+ })) |
|
| 474 |
+} |
|
| 475 |
+ |
|
| 476 |
+fn entitlements_actions(changed: bool, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 477 |
+ div() |
|
| 478 |
+ .flex_none() |
|
| 479 |
+ .flex() |
|
| 480 |
+ .items_center() |
|
| 481 |
+ .gap_1() |
|
| 482 |
+ .child(entitlement_icon_button( |
|
| 483 |
+ "add-entitlement", |
|
| 484 |
+ "icons/plus.svg", |
|
| 485 |
+ cx.listener(SettingsWindow::add_entitlement), |
|
| 486 |
+ )) |
|
| 487 |
+ .child(entitlement_icon_button( |
|
| 488 |
+ "remove-entitlement", |
|
| 489 |
+ "icons/minus.svg", |
|
| 490 |
+ cx.listener(SettingsWindow::remove_entitlement), |
|
| 491 |
+ )) |
|
| 492 |
+ .child(entitlements_revert_slot(changed, cx)) |
|
| 493 |
+} |
|
| 494 |
+ |
|
| 495 |
+fn entitlement_icon_button( |
|
| 496 |
+ id: &'static str, |
|
| 497 |
+ icon: &'static str, |
|
| 498 |
+ listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, |
|
| 499 |
+) -> impl IntoElement {
|
|
| 500 |
+ div() |
|
| 501 |
+ .flex_none() |
|
| 502 |
+ .w_6() |
|
| 503 |
+ .h_6() |
|
| 504 |
+ .rounded_md() |
|
| 505 |
+ .bg(rgb(0xebf1f0)) |
|
| 506 |
+ .text_color(rgb(0x53666d)) |
|
| 507 |
+ .flex() |
|
| 508 |
+ .items_center() |
|
| 509 |
+ .justify_center() |
|
| 510 |
+ .cursor_pointer() |
|
| 511 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 512 |
+ .id(id) |
|
| 513 |
+ .on_click(listener) |
|
| 514 |
+ .child(lucide_icon(icon)) |
|
| 515 |
+} |
|
| 516 |
+ |
|
| 517 |
+fn entitlements_revert_slot(changed: bool, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 518 |
+ div() |
|
| 519 |
+ .flex_none() |
|
| 520 |
+ .w_6() |
|
| 521 |
+ .h_6() |
|
| 522 |
+ .rounded_md() |
|
| 523 |
+ .bg(rgb(0xebf1f0)) |
|
| 524 |
+ .text_color(rgb(0x53666d)) |
|
| 525 |
+ .flex() |
|
| 526 |
+ .items_center() |
|
| 527 |
+ .justify_center() |
|
| 528 |
+ .id("revert-entitlements")
|
|
| 529 |
+ .when(!changed, |this| this.opacity(0.)) |
|
| 530 |
+ .when(changed, |this| {
|
|
| 531 |
+ this.cursor_pointer() |
|
| 532 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 533 |
+ .on_click(cx.listener(SettingsWindow::revert_entitlements)) |
|
| 534 |
+ }) |
|
| 535 |
+ .child(lucide_icon("icons/rotate-ccw.svg"))
|
|
| 536 |
+} |
|
| 537 |
+ |
|
| 538 |
+fn entitlement_header_row() -> gpui::Div {
|
|
| 539 |
+ div() |
|
| 540 |
+ .grid() |
|
| 541 |
+ .grid_cols(3) |
|
| 542 |
+ .px_3() |
|
| 543 |
+ .py_2() |
|
| 544 |
+ .bg(rgb(0xf6f9f9)) |
|
| 545 |
+ .border_b_1() |
|
| 546 |
+ .border_color(rgb(0xd8e0df)) |
|
| 547 |
+ .text_xs() |
|
| 548 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 549 |
+ .text_color(rgb(0x66767c)) |
|
| 550 |
+ .child("Key")
|
|
| 551 |
+ .child("Type")
|
|
| 552 |
+ .child("Value")
|
|
| 553 |
+} |
|
| 554 |
+ |
|
| 555 |
+fn entitlement_row( |
|
| 556 |
+ index: usize, |
|
| 557 |
+ entitlement: &AppEntitlement, |
|
| 558 |
+ selected: bool, |
|
| 559 |
+ changed: bool, |
|
| 560 |
+ edit: Option<&EntitlementEdit>, |
|
| 561 |
+ type_picker_open: bool, |
|
| 562 |
+ cx: &mut Context<SettingsWindow>, |
|
| 563 |
+) -> gpui::Div {
|
|
| 564 |
+ div() |
|
| 565 |
+ .grid() |
|
| 566 |
+ .grid_cols(3) |
|
| 567 |
+ .px_3() |
|
| 568 |
+ .py_1() |
|
| 569 |
+ .border_b_1() |
|
| 570 |
+ .border_color(rgb(0xe8eeee)) |
|
| 571 |
+ .text_xs() |
|
| 572 |
+ .text_color(rgb(0x405057)) |
|
| 573 |
+ .bg(if selected {
|
|
| 574 |
+ rgb(0xf0fbfa) |
|
| 575 |
+ } else {
|
|
| 576 |
+ rgb(0xffffff) |
|
| 577 |
+ }) |
|
| 578 |
+ .child(entitlement_cell( |
|
| 579 |
+ index, |
|
| 580 |
+ EntitlementField::Key, |
|
| 581 |
+ entitlement, |
|
| 582 |
+ edit, |
|
| 583 |
+ type_picker_open, |
|
| 584 |
+ false, |
|
| 585 |
+ cx, |
|
| 586 |
+ )) |
|
| 587 |
+ .child(entitlement_cell( |
|
| 588 |
+ index, |
|
| 589 |
+ EntitlementField::ValueType, |
|
| 590 |
+ entitlement, |
|
| 591 |
+ edit, |
|
| 592 |
+ type_picker_open, |
|
| 593 |
+ false, |
|
| 594 |
+ cx, |
|
| 595 |
+ )) |
|
| 596 |
+ .child(entitlement_cell( |
|
| 597 |
+ index, |
|
| 598 |
+ EntitlementField::Value, |
|
| 599 |
+ entitlement, |
|
| 600 |
+ edit, |
|
| 601 |
+ type_picker_open, |
|
| 602 |
+ changed, |
|
| 603 |
+ cx, |
|
| 604 |
+ )) |
|
| 605 |
+} |
|
| 606 |
+ |
|
| 607 |
+fn entitlement_cell( |
|
| 608 |
+ row: usize, |
|
| 609 |
+ field: EntitlementField, |
|
| 610 |
+ entitlement: &AppEntitlement, |
|
| 611 |
+ edit: Option<&EntitlementEdit>, |
|
| 612 |
+ type_picker_open: bool, |
|
| 613 |
+ row_revert_visible: bool, |
|
| 614 |
+ cx: &mut Context<SettingsWindow>, |
|
| 615 |
+) -> impl IntoElement {
|
|
| 616 |
+ let is_editing = edit.is_some_and(|edit| edit.row() == row && edit.field() == field); |
|
| 617 |
+ div() |
|
| 618 |
+ .min_w_0() |
|
| 619 |
+ .min_h_7() |
|
| 620 |
+ .flex() |
|
| 621 |
+ .items_center() |
|
| 622 |
+ .gap_1() |
|
| 623 |
+ .rounded_sm() |
|
| 624 |
+ .cursor_pointer() |
|
| 625 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 626 |
+ .id(("entitlement-cell", row * 3 + entitlement_field_id(field)))
|
|
| 627 |
+ .on_click(cx.listener(move |settings, event, window, cx| {
|
|
| 628 |
+ settings.begin_entitlement_edit(row, field, event, window, cx) |
|
| 629 |
+ })) |
|
| 630 |
+ .child( |
|
| 631 |
+ if let Some(EntitlementEdit::Text { input, .. }) =
|
|
| 632 |
+ edit.filter(|edit| edit.row() == row && edit.field() == field) |
|
| 633 |
+ {
|
|
| 634 |
+ machine_identity_editor(input) |
|
| 635 |
+ } else if let Some(EntitlementEdit::Type { selected, .. }) =
|
|
| 636 |
+ edit.filter(|edit| edit.row() == row && edit.field() == field) |
|
| 637 |
+ {
|
|
| 638 |
+ entitlement_type_editor(selected, type_picker_open, cx) |
|
| 639 |
+ } else if let Some(EntitlementEdit::Array { items, .. }) =
|
|
| 640 |
+ edit.filter(|edit| edit.row() == row && edit.field() == field) |
|
| 641 |
+ {
|
|
| 642 |
+ array_entitlement_editor(items, cx) |
|
| 643 |
+ } else if let Some(EntitlementEdit::Boolean { value, .. }) =
|
|
| 644 |
+ edit.filter(|edit| edit.row() == row && edit.field() == field) |
|
| 645 |
+ {
|
|
| 646 |
+ boolean_entitlement_editor(*value, cx) |
|
| 647 |
+ } else {
|
|
| 648 |
+ div() |
|
| 649 |
+ .min_w_0() |
|
| 650 |
+ .flex_1() |
|
| 651 |
+ .text_ellipsis() |
|
| 652 |
+ .child(entitlement_field_value(entitlement, field).clone()) |
|
| 653 |
+ }, |
|
| 654 |
+ ) |
|
| 655 |
+ .when( |
|
| 656 |
+ row_revert_visible && field == EntitlementField::Value && !is_editing, |
|
| 657 |
+ |this| this.child(entitlement_row_revert_button(row, cx)), |
|
| 658 |
+ ) |
|
| 659 |
+ .when(is_editing, |this| {
|
|
| 660 |
+ this.child(entitlement_edit_button( |
|
| 661 |
+ ("save-entitlement", row * 3 + entitlement_field_id(field)),
|
|
| 662 |
+ "icons/check.svg", |
|
| 663 |
+ cx.listener(SettingsWindow::save_entitlement_edit_from_button), |
|
| 664 |
+ )) |
|
| 665 |
+ .child(entitlement_edit_button( |
|
| 666 |
+ ("cancel-entitlement", row * 3 + entitlement_field_id(field)),
|
|
| 667 |
+ "icons/x.svg", |
|
| 668 |
+ cx.listener(SettingsWindow::cancel_entitlement_edit), |
|
| 669 |
+ )) |
|
| 670 |
+ }) |
|
| 671 |
+} |
|
| 672 |
+ |
|
| 673 |
+fn entitlement_row_revert_button(row: usize, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 674 |
+ div() |
|
| 675 |
+ .id(("revert-entitlement", row))
|
|
| 676 |
+ .flex_none() |
|
| 677 |
+ .w_6() |
|
| 678 |
+ .h_6() |
|
| 679 |
+ .rounded_md() |
|
| 680 |
+ .bg(rgb(0xebf1f0)) |
|
| 681 |
+ .text_color(rgb(0x53666d)) |
|
| 682 |
+ .flex() |
|
| 683 |
+ .items_center() |
|
| 684 |
+ .justify_center() |
|
| 685 |
+ .cursor_pointer() |
|
| 686 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 687 |
+ .on_click(cx.listener(move |settings, event, window, cx| {
|
|
| 688 |
+ settings.revert_entitlement(row, event, window, cx) |
|
| 689 |
+ })) |
|
| 690 |
+ .child(lucide_icon("icons/rotate-ccw.svg"))
|
|
| 691 |
+} |
|
| 692 |
+ |
|
| 693 |
+fn boolean_entitlement_editor(value: bool, cx: &mut Context<SettingsWindow>) -> gpui::Div {
|
|
| 694 |
+ div().min_w_0().flex_1().child( |
|
| 695 |
+ div() |
|
| 696 |
+ .id("boolean-entitlement-editor")
|
|
| 697 |
+ .flex() |
|
| 698 |
+ .items_center() |
|
| 699 |
+ .gap_2() |
|
| 700 |
+ .cursor_pointer() |
|
| 701 |
+ .on_click(cx.listener(SettingsWindow::toggle_boolean_entitlement_value)) |
|
| 702 |
+ .child( |
|
| 703 |
+ div() |
|
| 704 |
+ .flex_none() |
|
| 705 |
+ .w_5() |
|
| 706 |
+ .h_5() |
|
| 707 |
+ .rounded_sm() |
|
| 708 |
+ .border_1() |
|
| 709 |
+ .border_color(if value { rgb(0x168291) } else { rgb(0xcfd8d6) })
|
|
| 710 |
+ .bg(if value { rgb(0xd3eadc) } else { rgb(0xffffff) })
|
|
| 711 |
+ .text_color(rgb(0x1d6b45)) |
|
| 712 |
+ .flex() |
|
| 713 |
+ .items_center() |
|
| 714 |
+ .justify_center() |
|
| 715 |
+ .when(value, |this| this.child(lucide_icon("icons/check.svg"))),
|
|
| 716 |
+ ) |
|
| 717 |
+ .child( |
|
| 718 |
+ div() |
|
| 719 |
+ .min_w_0() |
|
| 720 |
+ .text_xs() |
|
| 721 |
+ .text_color(rgb(0x405057)) |
|
| 722 |
+ .child(if value { "true" } else { "false" }),
|
|
| 723 |
+ ), |
|
| 724 |
+ ) |
|
| 725 |
+} |
|
| 726 |
+ |
|
| 727 |
+fn array_entitlement_editor( |
|
| 728 |
+ items: &[Entity<EditLine>], |
|
| 729 |
+ cx: &mut Context<SettingsWindow>, |
|
| 730 |
+) -> gpui::Div {
|
|
| 731 |
+ div() |
|
| 732 |
+ .min_w_0() |
|
| 733 |
+ .flex_1() |
|
| 734 |
+ .flex() |
|
| 735 |
+ .flex_col() |
|
| 736 |
+ .gap_1() |
|
| 737 |
+ .children(items.iter().enumerate().map(|(index, input)| {
|
|
| 738 |
+ div() |
|
| 739 |
+ .min_w_0() |
|
| 740 |
+ .flex() |
|
| 741 |
+ .items_center() |
|
| 742 |
+ .gap_1() |
|
| 743 |
+ .child(machine_identity_editor(input)) |
|
| 744 |
+ .child(array_entitlement_item_button( |
|
| 745 |
+ ("remove-entitlement-array-item", index),
|
|
| 746 |
+ "icons/minus.svg", |
|
| 747 |
+ cx.listener(move |settings, event, window, cx| {
|
|
| 748 |
+ settings.remove_entitlement_array_item(index, event, window, cx) |
|
| 749 |
+ }), |
|
| 750 |
+ )) |
|
| 751 |
+ })) |
|
| 752 |
+ .child( |
|
| 753 |
+ div() |
|
| 754 |
+ .id("add-entitlement-array-item")
|
|
| 755 |
+ .h_6() |
|
| 756 |
+ .px_2() |
|
| 757 |
+ .rounded_md() |
|
| 758 |
+ .bg(rgb(0xebf1f0)) |
|
| 759 |
+ .text_color(rgb(0x53666d)) |
|
| 760 |
+ .text_xs() |
|
| 761 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 762 |
+ .flex() |
|
| 763 |
+ .items_center() |
|
| 764 |
+ .justify_center() |
|
| 765 |
+ .gap_1() |
|
| 766 |
+ .cursor_pointer() |
|
| 767 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 768 |
+ .on_click(cx.listener(SettingsWindow::add_entitlement_array_item)) |
|
| 769 |
+ .child(lucide_icon("icons/plus.svg"))
|
|
| 770 |
+ .child("Add item"),
|
|
| 771 |
+ ) |
|
| 772 |
+} |
|
| 773 |
+ |
|
| 774 |
+fn array_entitlement_item_button( |
|
| 775 |
+ id: (&'static str, usize), |
|
| 776 |
+ icon: &'static str, |
|
| 777 |
+ listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, |
|
| 778 |
+) -> impl IntoElement {
|
|
| 779 |
+ div() |
|
| 780 |
+ .id(id) |
|
| 781 |
+ .flex_none() |
|
| 782 |
+ .w_6() |
|
| 783 |
+ .h_6() |
|
| 784 |
+ .rounded_md() |
|
| 785 |
+ .bg(rgb(0xebf1f0)) |
|
| 786 |
+ .text_color(rgb(0x53666d)) |
|
| 787 |
+ .flex() |
|
| 788 |
+ .items_center() |
|
| 789 |
+ .justify_center() |
|
| 790 |
+ .cursor_pointer() |
|
| 791 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 792 |
+ .on_click(listener) |
|
| 793 |
+ .child(lucide_icon(icon)) |
|
| 794 |
+} |
|
| 795 |
+ |
|
| 796 |
+fn entitlement_type_editor( |
|
| 797 |
+ selected: &SharedString, |
|
| 798 |
+ open: bool, |
|
| 799 |
+ cx: &mut Context<SettingsWindow>, |
|
| 800 |
+) -> gpui::Div {
|
|
| 801 |
+ div() |
|
| 802 |
+ .relative() |
|
| 803 |
+ .min_w_0() |
|
| 804 |
+ .flex_1() |
|
| 805 |
+ .child( |
|
| 806 |
+ div() |
|
| 807 |
+ .id("entitlement-type-combobox")
|
|
| 808 |
+ .h_7() |
|
| 809 |
+ .px_2() |
|
| 810 |
+ .rounded_md() |
|
| 811 |
+ .border_1() |
|
| 812 |
+ .border_color(if open { rgb(0x0f6f7a) } else { rgb(0xcfd8d6) })
|
|
| 813 |
+ .bg(rgb(0xffffff)) |
|
| 814 |
+ .flex() |
|
| 815 |
+ .items_center() |
|
| 816 |
+ .gap_2() |
|
| 817 |
+ .cursor_pointer() |
|
| 818 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 819 |
+ .on_click(cx.listener(SettingsWindow::toggle_entitlement_type_picker)) |
|
| 820 |
+ .child( |
|
| 821 |
+ div() |
|
| 822 |
+ .min_w_0() |
|
| 823 |
+ .flex_1() |
|
| 824 |
+ .text_ellipsis() |
|
| 825 |
+ .text_xs() |
|
| 826 |
+ .text_color(rgb(0x405057)) |
|
| 827 |
+ .child(selected.clone()), |
|
| 828 |
+ ) |
|
| 829 |
+ .child(chevron(open)), |
|
| 830 |
+ ) |
|
| 831 |
+ .when(open, |this| {
|
|
| 832 |
+ this.child( |
|
| 833 |
+ div().absolute().top_0().left_0().w(px(0.)).h(px(0.)).child( |
|
| 834 |
+ deferred( |
|
| 835 |
+ anchored() |
|
| 836 |
+ .snap_to_window_with_margin(px(8.)) |
|
| 837 |
+ .position_mode(AnchoredPositionMode::Local) |
|
| 838 |
+ .position(point(px(0.), px(34.))) |
|
| 839 |
+ .child( |
|
| 840 |
+ div() |
|
| 841 |
+ .w(px(160.)) |
|
| 842 |
+ .rounded_md() |
|
| 843 |
+ .border_1() |
|
| 844 |
+ .border_color(rgb(0xd8e0df)) |
|
| 845 |
+ .bg(rgb(0xf8fbfa)) |
|
| 846 |
+ .occlude() |
|
| 847 |
+ .child(dropdown_list( |
|
| 848 |
+ EntitlementValue::EDITABLE_TYPE_LABELS |
|
| 849 |
+ .into_iter() |
|
| 850 |
+ .enumerate() |
|
| 851 |
+ .map(|(index, label)| {
|
|
| 852 |
+ entitlement_type_option( |
|
| 853 |
+ index, |
|
| 854 |
+ label, |
|
| 855 |
+ label == selected.as_ref(), |
|
| 856 |
+ cx, |
|
| 857 |
+ ) |
|
| 858 |
+ }), |
|
| 859 |
+ )), |
|
| 860 |
+ ), |
|
| 861 |
+ ) |
|
| 862 |
+ .with_priority(10), |
|
| 863 |
+ ), |
|
| 864 |
+ ) |
|
| 865 |
+ }) |
|
| 866 |
+} |
|
| 867 |
+ |
|
| 868 |
+fn entitlement_type_option( |
|
| 869 |
+ index: usize, |
|
| 870 |
+ label: &'static str, |
|
| 871 |
+ selected: bool, |
|
| 872 |
+ cx: &mut Context<SettingsWindow>, |
|
| 873 |
+) -> impl IntoElement {
|
|
| 874 |
+ div() |
|
| 875 |
+ .id(("entitlement-type-option", index))
|
|
| 876 |
+ .h_7() |
|
| 877 |
+ .px_2() |
|
| 878 |
+ .rounded_sm() |
|
| 879 |
+ .bg(if selected {
|
|
| 880 |
+ rgb(0xf0fbfa) |
|
| 881 |
+ } else {
|
|
| 882 |
+ rgb(0xffffff) |
|
| 883 |
+ }) |
|
| 884 |
+ .text_color(rgb(if selected { 0x173f45 } else { 0x405057 }))
|
|
| 885 |
+ .text_xs() |
|
| 886 |
+ .font_weight(if selected {
|
|
| 887 |
+ FontWeight::SEMIBOLD |
|
| 888 |
+ } else {
|
|
| 889 |
+ FontWeight::NORMAL |
|
| 890 |
+ }) |
|
| 891 |
+ .flex() |
|
| 892 |
+ .items_center() |
|
| 893 |
+ .cursor_pointer() |
|
| 894 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 895 |
+ .on_click(cx.listener(move |settings, event, window, cx| {
|
|
| 896 |
+ settings.select_entitlement_type(label, event, window, cx) |
|
| 897 |
+ })) |
|
| 898 |
+ .child(label) |
|
| 899 |
+} |
|
| 900 |
+ |
|
| 901 |
+fn entitlement_field_id(field: EntitlementField) -> usize {
|
|
| 902 |
+ match field {
|
|
| 903 |
+ EntitlementField::Key => 0, |
|
| 904 |
+ EntitlementField::ValueType => 1, |
|
| 905 |
+ EntitlementField::Value => 2, |
|
| 906 |
+ } |
|
| 907 |
+} |
|
| 908 |
+ |
|
| 909 |
+fn entitlement_edit_button( |
|
| 910 |
+ id: (&'static str, usize), |
|
| 911 |
+ icon: &'static str, |
|
| 912 |
+ listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, |
|
| 913 |
+) -> impl IntoElement {
|
|
| 914 |
+ div() |
|
| 915 |
+ .id(id) |
|
| 916 |
+ .flex_none() |
|
| 917 |
+ .w_6() |
|
| 918 |
+ .h_6() |
|
| 919 |
+ .rounded_md() |
|
| 920 |
+ .bg(rgb(0xebf1f0)) |
|
| 921 |
+ .text_color(rgb(0x53666d)) |
|
| 922 |
+ .flex() |
|
| 923 |
+ .items_center() |
|
| 924 |
+ .justify_center() |
|
| 925 |
+ .cursor_pointer() |
|
| 926 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 927 |
+ .on_click(listener) |
|
| 928 |
+ .child(lucide_icon(icon)) |
|
| 929 |
+} |
| ... | ... |
@@ -0,0 +1,252 @@ |
| 1 |
+use super::*; |
|
| 2 |
+ |
|
| 3 |
+pub(super) fn render( |
|
| 4 |
+ focus_handle: &FocusHandle, |
|
| 5 |
+ scroll_handle: &ScrollHandle, |
|
| 6 |
+ login: &DeveloperLoginState, |
|
| 7 |
+ cx: &mut Context<SettingsWindow>, |
|
| 8 |
+) -> gpui::Div {
|
|
| 9 |
+ let content = div().flex().flex_col().gap_4().child(match login.step {
|
|
| 10 |
+ DeveloperLoginStep::Credentials => credentials_form(login, cx), |
|
| 11 |
+ DeveloperLoginStep::TwoFactor => two_factor_form(login, cx), |
|
| 12 |
+ }); |
|
| 13 |
+ |
|
| 14 |
+ settings_window_shell() |
|
| 15 |
+ .track_focus(focus_handle) |
|
| 16 |
+ .capture_key_down(cx.listener(SettingsWindow::handle_developer_login_key)) |
|
| 17 |
+ .gap_4() |
|
| 18 |
+ .child(settings_window_header( |
|
| 19 |
+ "Add Apple Account", |
|
| 20 |
+ "Sign in before choosing developer teams, App IDs, and signing resources.", |
|
| 21 |
+ )) |
|
| 22 |
+ .child(scroll_panel( |
|
| 23 |
+ "developer-login-scroll", |
|
| 24 |
+ scroll_handle, |
|
| 25 |
+ content, |
|
| 26 |
+ )) |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+fn credentials_form(login: &DeveloperLoginState, cx: &mut Context<SettingsWindow>) -> gpui::Div {
|
|
| 30 |
+ developer_login_card() |
|
| 31 |
+ .child(login_error(login.error.as_ref())) |
|
| 32 |
+ .child(login_input("Email", &login.email))
|
|
| 33 |
+ .child(login_input("Password", &login.password))
|
|
| 34 |
+ .child(remember_account_checkbox(login.remember_account, cx)) |
|
| 35 |
+ .child(primary_login_button( |
|
| 36 |
+ "Continue", |
|
| 37 |
+ cx.listener(SettingsWindow::submit_developer_login), |
|
| 38 |
+ )) |
|
| 39 |
+ .child(mock_accounts_note()) |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+fn two_factor_form(login: &DeveloperLoginState, cx: &mut Context<SettingsWindow>) -> gpui::Div {
|
|
| 43 |
+ developer_login_card() |
|
| 44 |
+ .child( |
|
| 45 |
+ div() |
|
| 46 |
+ .p_3() |
|
| 47 |
+ .rounded_md() |
|
| 48 |
+ .bg(rgb(0xf6f9f9)) |
|
| 49 |
+ .flex() |
|
| 50 |
+ .flex_col() |
|
| 51 |
+ .gap_1() |
|
| 52 |
+ .child( |
|
| 53 |
+ div() |
|
| 54 |
+ .text_sm() |
|
| 55 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 56 |
+ .text_color(rgb(0x24333a)) |
|
| 57 |
+ .child("Two-Factor Authentication"),
|
|
| 58 |
+ ) |
|
| 59 |
+ .child( |
|
| 60 |
+ div() |
|
| 61 |
+ .text_xs() |
|
| 62 |
+ .text_color(rgb(0x6a7a81)) |
|
| 63 |
+ .child(login.two_factor_detail.clone()), |
|
| 64 |
+ ), |
|
| 65 |
+ ) |
|
| 66 |
+ .child(login_error(login.error.as_ref())) |
|
| 67 |
+ .child(login_input("Verification code", &login.code))
|
|
| 68 |
+ .child( |
|
| 69 |
+ div() |
|
| 70 |
+ .flex() |
|
| 71 |
+ .items_center() |
|
| 72 |
+ .gap_2() |
|
| 73 |
+ .child(secondary_login_button( |
|
| 74 |
+ "Back", |
|
| 75 |
+ cx.listener(SettingsWindow::back_to_developer_login), |
|
| 76 |
+ )) |
|
| 77 |
+ .child(primary_login_button( |
|
| 78 |
+ "Verify", |
|
| 79 |
+ cx.listener(SettingsWindow::submit_developer_two_factor), |
|
| 80 |
+ )), |
|
| 81 |
+ ) |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+fn developer_login_card() -> gpui::Div {
|
|
| 85 |
+ div() |
|
| 86 |
+ .p_4() |
|
| 87 |
+ .rounded_md() |
|
| 88 |
+ .border_1() |
|
| 89 |
+ .border_color(rgb(0xd8e0df)) |
|
| 90 |
+ .bg(rgb(0xffffff)) |
|
| 91 |
+ .flex() |
|
| 92 |
+ .flex_col() |
|
| 93 |
+ .gap_3() |
|
| 94 |
+} |
|
| 95 |
+ |
|
| 96 |
+fn login_input(label: &'static str, input: &Entity<EditLine>) -> gpui::Div {
|
|
| 97 |
+ div() |
|
| 98 |
+ .flex() |
|
| 99 |
+ .flex_col() |
|
| 100 |
+ .gap_1() |
|
| 101 |
+ .child(settings_label(label)) |
|
| 102 |
+ .child( |
|
| 103 |
+ div() |
|
| 104 |
+ .h_8() |
|
| 105 |
+ .px_2() |
|
| 106 |
+ .rounded_md() |
|
| 107 |
+ .border_1() |
|
| 108 |
+ .border_color(rgb(0xcdd8d6)) |
|
| 109 |
+ .bg(rgb(0xffffff)) |
|
| 110 |
+ .text_sm() |
|
| 111 |
+ .text_color(rgb(0x24333a)) |
|
| 112 |
+ .flex() |
|
| 113 |
+ .items_center() |
|
| 114 |
+ .overflow_hidden() |
|
| 115 |
+ .child(input.clone()), |
|
| 116 |
+ ) |
|
| 117 |
+} |
|
| 118 |
+ |
|
| 119 |
+fn login_error(error: Option<&SharedString>) -> gpui::Div {
|
|
| 120 |
+ div().when_some(error, |this, error| {
|
|
| 121 |
+ this.p_3() |
|
| 122 |
+ .rounded_md() |
|
| 123 |
+ .border_1() |
|
| 124 |
+ .border_color(rgb(0xe8c6c2)) |
|
| 125 |
+ .bg(rgb(0xfff7f6)) |
|
| 126 |
+ .text_sm() |
|
| 127 |
+ .text_color(rgb(0x8b302b)) |
|
| 128 |
+ .child(error.clone()) |
|
| 129 |
+ }) |
|
| 130 |
+} |
|
| 131 |
+ |
|
| 132 |
+fn remember_account_checkbox(checked: bool, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 133 |
+ div() |
|
| 134 |
+ .id("remember-developer-account")
|
|
| 135 |
+ .p_2() |
|
| 136 |
+ .rounded_md() |
|
| 137 |
+ .bg(rgb(0xf6f9f9)) |
|
| 138 |
+ .flex() |
|
| 139 |
+ .items_center() |
|
| 140 |
+ .gap_2() |
|
| 141 |
+ .cursor_pointer() |
|
| 142 |
+ .hover(|style| style.bg(rgb(0xebf1f0))) |
|
| 143 |
+ .on_click(cx.listener(SettingsWindow::toggle_remember_developer_account)) |
|
| 144 |
+ .child( |
|
| 145 |
+ div() |
|
| 146 |
+ .flex_none() |
|
| 147 |
+ .w_4() |
|
| 148 |
+ .h_4() |
|
| 149 |
+ .rounded_sm() |
|
| 150 |
+ .border_1() |
|
| 151 |
+ .border_color(if checked {
|
|
| 152 |
+ rgb(0x173f45) |
|
| 153 |
+ } else {
|
|
| 154 |
+ rgb(0xaebcba) |
|
| 155 |
+ }) |
|
| 156 |
+ .bg(if checked {
|
|
| 157 |
+ rgb(0x173f45) |
|
| 158 |
+ } else {
|
|
| 159 |
+ rgb(0xffffff) |
|
| 160 |
+ }) |
|
| 161 |
+ .flex() |
|
| 162 |
+ .items_center() |
|
| 163 |
+ .justify_center() |
|
| 164 |
+ .when(checked, |this| {
|
|
| 165 |
+ this.text_color(rgb(0xffffff)) |
|
| 166 |
+ .child(lucide_icon("icons/check.svg"))
|
|
| 167 |
+ }), |
|
| 168 |
+ ) |
|
| 169 |
+ .child( |
|
| 170 |
+ div() |
|
| 171 |
+ .min_w_0() |
|
| 172 |
+ .flex() |
|
| 173 |
+ .flex_col() |
|
| 174 |
+ .gap_0p5() |
|
| 175 |
+ .child( |
|
| 176 |
+ div() |
|
| 177 |
+ .text_xs() |
|
| 178 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 179 |
+ .text_color(rgb(0x24333a)) |
|
| 180 |
+ .child("Save my account"),
|
|
| 181 |
+ ) |
|
| 182 |
+ .child( |
|
| 183 |
+ div() |
|
| 184 |
+ .text_xs() |
|
| 185 |
+ .text_color(rgb(0x6a7a81)) |
|
| 186 |
+ .child("Use the account ID for system keyring storage when login is real."),
|
|
| 187 |
+ ), |
|
| 188 |
+ ) |
|
| 189 |
+} |
|
| 190 |
+ |
|
| 191 |
+fn primary_login_button( |
|
| 192 |
+ label: &'static str, |
|
| 193 |
+ listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, |
|
| 194 |
+) -> impl IntoElement {
|
|
| 195 |
+ div() |
|
| 196 |
+ .id("developer-login-primary")
|
|
| 197 |
+ .h_9() |
|
| 198 |
+ .rounded_md() |
|
| 199 |
+ .bg(rgb(0x173f45)) |
|
| 200 |
+ .text_color(rgb(0xffffff)) |
|
| 201 |
+ .text_sm() |
|
| 202 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 203 |
+ .flex() |
|
| 204 |
+ .items_center() |
|
| 205 |
+ .justify_center() |
|
| 206 |
+ .cursor_pointer() |
|
| 207 |
+ .hover(|style| style.bg(rgb(0x24565d))) |
|
| 208 |
+ .on_click(listener) |
|
| 209 |
+ .child(label) |
|
| 210 |
+} |
|
| 211 |
+ |
|
| 212 |
+fn secondary_login_button( |
|
| 213 |
+ label: &'static str, |
|
| 214 |
+ listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, |
|
| 215 |
+) -> impl IntoElement {
|
|
| 216 |
+ div() |
|
| 217 |
+ .id("developer-login-secondary")
|
|
| 218 |
+ .h_9() |
|
| 219 |
+ .px_4() |
|
| 220 |
+ .rounded_md() |
|
| 221 |
+ .bg(rgb(0xebf1f0)) |
|
| 222 |
+ .text_color(rgb(0x53666d)) |
|
| 223 |
+ .text_sm() |
|
| 224 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 225 |
+ .flex() |
|
| 226 |
+ .items_center() |
|
| 227 |
+ .justify_center() |
|
| 228 |
+ .cursor_pointer() |
|
| 229 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 230 |
+ .on_click(listener) |
|
| 231 |
+ .child(label) |
|
| 232 |
+} |
|
| 233 |
+ |
|
| 234 |
+fn mock_accounts_note() -> gpui::Div {
|
|
| 235 |
+ div() |
|
| 236 |
+ .p_3() |
|
| 237 |
+ .rounded_md() |
|
| 238 |
+ .bg(rgb(0xf6f9f9)) |
|
| 239 |
+ .text_xs() |
|
| 240 |
+ .text_color(rgb(0x6a7a81)) |
|
| 241 |
+ .flex() |
|
| 242 |
+ .flex_col() |
|
| 243 |
+ .gap_1() |
|
| 244 |
+ .child( |
|
| 245 |
+ div() |
|
| 246 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 247 |
+ .text_color(rgb(0x53666d)) |
|
| 248 |
+ .child("Mock accounts"),
|
|
| 249 |
+ ) |
|
| 250 |
+ .child("a@example.com requires 2FA code 123456.")
|
|
| 251 |
+ .child("b@example.com signs in without 2FA.")
|
|
| 252 |
+} |
| ... | ... |
@@ -0,0 +1,422 @@ |
| 1 |
+use super::*; |
|
| 2 |
+ |
|
| 3 |
+pub(super) fn render( |
|
| 4 |
+ focus_handle: &FocusHandle, |
|
| 5 |
+ scroll_handle: &ScrollHandle, |
|
| 6 |
+ teams: &[TeamOption], |
|
| 7 |
+ selected_team: usize, |
|
| 8 |
+ auto_app_id: bool, |
|
| 9 |
+ selected_app_id: usize, |
|
| 10 |
+ team_picker_open: bool, |
|
| 11 |
+ app_id_picker_open: bool, |
|
| 12 |
+ cx: &mut Context<SettingsWindow>, |
|
| 13 |
+) -> gpui::Div {
|
|
| 14 |
+ let selected = selected_team; |
|
| 15 |
+ let team = teams.get(selected); |
|
| 16 |
+ let mut content = div() |
|
| 17 |
+ .flex() |
|
| 18 |
+ .flex_col() |
|
| 19 |
+ .gap_4() |
|
| 20 |
+ .when_some(team, |this, team| {
|
|
| 21 |
+ this.child(developer_settings_section( |
|
| 22 |
+ "Developer Team", |
|
| 23 |
+ "Team used for certificates, profiles, and signing identities.", |
|
| 24 |
+ div() |
|
| 25 |
+ .flex() |
|
| 26 |
+ .flex_col() |
|
| 27 |
+ .gap_3() |
|
| 28 |
+ .child(combo_with_popover( |
|
| 29 |
+ "team-popover-scroll", |
|
| 30 |
+ combo_button( |
|
| 31 |
+ team.identifier.clone(), |
|
| 32 |
+ team.name.clone(), |
|
| 33 |
+ developer_account_title(&team.role), |
|
| 34 |
+ team_picker_open, |
|
| 35 |
+ ) |
|
| 36 |
+ .id("settings-team-combobox")
|
|
| 37 |
+ .on_click(cx.listener(SettingsWindow::toggle_team_picker)), |
|
| 38 |
+ team_picker_open, |
|
| 39 |
+ dropdown_list((0..teams.len()).map(|index| {
|
|
| 40 |
+ settings_team_row(&teams[index], index == selected) |
|
| 41 |
+ .id(("settings-team-row", index))
|
|
| 42 |
+ .on_click(cx.listener(move |this, event, window, cx| {
|
|
| 43 |
+ this.select_team(index, event, window, cx) |
|
| 44 |
+ })) |
|
| 45 |
+ })) |
|
| 46 |
+ .id("settings-team-options"),
|
|
| 47 |
+ )) |
|
| 48 |
+ .child(properties_list(vec![ |
|
| 49 |
+ ("Name", team.name.to_string()),
|
|
| 50 |
+ ("Team ID", team.identifier.to_string()),
|
|
| 51 |
+ ("Role", team.role.to_string()),
|
|
| 52 |
+ ])), |
|
| 53 |
+ )) |
|
| 54 |
+ }); |
|
| 55 |
+ |
|
| 56 |
+ if let Some(team) = team {
|
|
| 57 |
+ content = content.child(app_id_section( |
|
| 58 |
+ team, |
|
| 59 |
+ auto_app_id, |
|
| 60 |
+ selected_app_id, |
|
| 61 |
+ app_id_picker_open, |
|
| 62 |
+ cx, |
|
| 63 |
+ )); |
|
| 64 |
+ } |
|
| 65 |
+ content = content.child(developer_logout_button(cx)); |
|
| 66 |
+ |
|
| 67 |
+ settings_window_shell() |
|
| 68 |
+ .track_focus(focus_handle) |
|
| 69 |
+ .capture_key_down(cx.listener(SettingsWindow::handle_machine_identity_editor_key)) |
|
| 70 |
+ .gap_4() |
|
| 71 |
+ .child(settings_window_header_with_action( |
|
| 72 |
+ settings_window_header( |
|
| 73 |
+ "Developer Settings", |
|
| 74 |
+ "Configure developer-team resources used during signing.", |
|
| 75 |
+ ), |
|
| 76 |
+ team_refresh_button(cx), |
|
| 77 |
+ )) |
|
| 78 |
+ .child(scroll_panel("team-settings-scroll", scroll_handle, content))
|
|
| 79 |
+} |
|
| 80 |
+ |
|
| 81 |
+fn team_refresh_button(cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 82 |
+ div() |
|
| 83 |
+ .id("refresh-team-details")
|
|
| 84 |
+ .flex_none() |
|
| 85 |
+ .h_8() |
|
| 86 |
+ .px_3() |
|
| 87 |
+ .rounded_md() |
|
| 88 |
+ .bg(rgb(0xebf1f0)) |
|
| 89 |
+ .text_color(rgb(0x53666d)) |
|
| 90 |
+ .text_xs() |
|
| 91 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 92 |
+ .flex() |
|
| 93 |
+ .items_center() |
|
| 94 |
+ .justify_center() |
|
| 95 |
+ .gap_2() |
|
| 96 |
+ .cursor_pointer() |
|
| 97 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 98 |
+ .on_click(cx.listener(SettingsWindow::refresh_team_details)) |
|
| 99 |
+ .child(lucide_icon("icons/refresh-cw.svg"))
|
|
| 100 |
+ .child("Refresh")
|
|
| 101 |
+} |
|
| 102 |
+ |
|
| 103 |
+fn developer_logout_button(cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 104 |
+ div() |
|
| 105 |
+ .id("developer-log-out")
|
|
| 106 |
+ .p_3() |
|
| 107 |
+ .rounded_md() |
|
| 108 |
+ .border_1() |
|
| 109 |
+ .border_color(rgb(0xe0d3d1)) |
|
| 110 |
+ .bg(rgb(0xffffff)) |
|
| 111 |
+ .text_color(rgb(0x7d3430)) |
|
| 112 |
+ .flex() |
|
| 113 |
+ .items_center() |
|
| 114 |
+ .justify_between() |
|
| 115 |
+ .gap_3() |
|
| 116 |
+ .cursor_pointer() |
|
| 117 |
+ .hover(|style| style.bg(rgb(0xfff7f6))) |
|
| 118 |
+ .on_click(cx.listener(SettingsWindow::log_out_developer_account)) |
|
| 119 |
+ .child( |
|
| 120 |
+ div() |
|
| 121 |
+ .min_w_0() |
|
| 122 |
+ .flex() |
|
| 123 |
+ .flex_col() |
|
| 124 |
+ .gap_0p5() |
|
| 125 |
+ .child( |
|
| 126 |
+ div() |
|
| 127 |
+ .text_sm() |
|
| 128 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 129 |
+ .child("Log Out"),
|
|
| 130 |
+ ) |
|
| 131 |
+ .child( |
|
| 132 |
+ div() |
|
| 133 |
+ .text_xs() |
|
| 134 |
+ .text_color(rgb(0x8b6b67)) |
|
| 135 |
+ .child("Remove this Apple Account session from Super Sideloader."),
|
|
| 136 |
+ ), |
|
| 137 |
+ ) |
|
| 138 |
+ .child( |
|
| 139 |
+ div() |
|
| 140 |
+ .flex_none() |
|
| 141 |
+ .w_7() |
|
| 142 |
+ .h_7() |
|
| 143 |
+ .rounded_md() |
|
| 144 |
+ .bg(rgb(0xf4e9e7)) |
|
| 145 |
+ .flex() |
|
| 146 |
+ .items_center() |
|
| 147 |
+ .justify_center() |
|
| 148 |
+ .child(lucide_icon("icons/log-out.svg")),
|
|
| 149 |
+ ) |
|
| 150 |
+} |
|
| 151 |
+ |
|
| 152 |
+fn developer_settings_section( |
|
| 153 |
+ title: &'static str, |
|
| 154 |
+ detail: &'static str, |
|
| 155 |
+ body: impl IntoElement, |
|
| 156 |
+) -> gpui::Div {
|
|
| 157 |
+ div() |
|
| 158 |
+ .p_3() |
|
| 159 |
+ .rounded_md() |
|
| 160 |
+ .border_1() |
|
| 161 |
+ .border_color(rgb(0xd8e0df)) |
|
| 162 |
+ .bg(rgb(0xffffff)) |
|
| 163 |
+ .flex() |
|
| 164 |
+ .flex_col() |
|
| 165 |
+ .gap_3() |
|
| 166 |
+ .child( |
|
| 167 |
+ div() |
|
| 168 |
+ .flex() |
|
| 169 |
+ .flex_col() |
|
| 170 |
+ .gap_0p5() |
|
| 171 |
+ .child( |
|
| 172 |
+ div() |
|
| 173 |
+ .text_sm() |
|
| 174 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 175 |
+ .text_color(rgb(0x24333a)) |
|
| 176 |
+ .child(title), |
|
| 177 |
+ ) |
|
| 178 |
+ .child(div().text_xs().text_color(rgb(0x6a7a81)).child(detail)), |
|
| 179 |
+ ) |
|
| 180 |
+ .child(body) |
|
| 181 |
+} |
|
| 182 |
+ |
|
| 183 |
+fn settings_team_row(team: &TeamOption, selected: bool) -> gpui::Div {
|
|
| 184 |
+ div() |
|
| 185 |
+ .h(px(COMBO_ITEM_HEIGHT)) |
|
| 186 |
+ .px_3() |
|
| 187 |
+ .rounded_md() |
|
| 188 |
+ .border_1() |
|
| 189 |
+ .border_color(if selected {
|
|
| 190 |
+ rgb(0x0f6f7a) |
|
| 191 |
+ } else {
|
|
| 192 |
+ rgb(0xd8e0df) |
|
| 193 |
+ }) |
|
| 194 |
+ .bg(if selected {
|
|
| 195 |
+ rgb(0xf0fbfa) |
|
| 196 |
+ } else {
|
|
| 197 |
+ rgb(0xffffff) |
|
| 198 |
+ }) |
|
| 199 |
+ .cursor_pointer() |
|
| 200 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 201 |
+ .flex() |
|
| 202 |
+ .items_center() |
|
| 203 |
+ .child(combo_item_content( |
|
| 204 |
+ team.identifier.clone(), |
|
| 205 |
+ team.name.clone(), |
|
| 206 |
+ developer_account_title(&team.role), |
|
| 207 |
+ )) |
|
| 208 |
+} |
|
| 209 |
+ |
|
| 210 |
+fn app_id_section( |
|
| 211 |
+ team: &TeamOption, |
|
| 212 |
+ auto_app_id: bool, |
|
| 213 |
+ selected_app_id: usize, |
|
| 214 |
+ picker_open: bool, |
|
| 215 |
+ cx: &mut Context<SettingsWindow>, |
|
| 216 |
+) -> gpui::Div {
|
|
| 217 |
+ let manual_enabled = !auto_app_id; |
|
| 218 |
+ let selected_app_id = selected_app_id.min(team.app_ids.len().saturating_sub(1)); |
|
| 219 |
+ let selected = team.app_ids.get(selected_app_id); |
|
| 220 |
+ |
|
| 221 |
+ developer_settings_section( |
|
| 222 |
+ "App ID", |
|
| 223 |
+ "Identifier used when creating or selecting the provisioning profile.", |
|
| 224 |
+ div() |
|
| 225 |
+ .flex() |
|
| 226 |
+ .flex_col() |
|
| 227 |
+ .gap_3() |
|
| 228 |
+ .child(auto_app_id_checkbox(auto_app_id, cx)) |
|
| 229 |
+ .child( |
|
| 230 |
+ div() |
|
| 231 |
+ .flex() |
|
| 232 |
+ .flex_col() |
|
| 233 |
+ .gap_2() |
|
| 234 |
+ .child(settings_label("Manual App ID"))
|
|
| 235 |
+ .child( |
|
| 236 |
+ div() |
|
| 237 |
+ .flex() |
|
| 238 |
+ .items_center() |
|
| 239 |
+ .gap_2() |
|
| 240 |
+ .when_some(selected, |this, selected| {
|
|
| 241 |
+ this.child( |
|
| 242 |
+ div() |
|
| 243 |
+ .min_w_0() |
|
| 244 |
+ .flex_1() |
|
| 245 |
+ .when(!manual_enabled, |this| this.opacity(0.5)) |
|
| 246 |
+ .child(combo_with_popover( |
|
| 247 |
+ "app-id-popover-scroll", |
|
| 248 |
+ combo_button( |
|
| 249 |
+ selected.identifier.clone(), |
|
| 250 |
+ selected.name.clone(), |
|
| 251 |
+ selected.kind.clone(), |
|
| 252 |
+ picker_open && manual_enabled, |
|
| 253 |
+ ) |
|
| 254 |
+ .id("app-id-combobox")
|
|
| 255 |
+ .when( |
|
| 256 |
+ manual_enabled, |
|
| 257 |
+ |this| {
|
|
| 258 |
+ this.on_click(cx.listener( |
|
| 259 |
+ SettingsWindow::toggle_app_id_picker, |
|
| 260 |
+ )) |
|
| 261 |
+ }, |
|
| 262 |
+ ), |
|
| 263 |
+ picker_open && manual_enabled, |
|
| 264 |
+ dropdown_list((0..team.app_ids.len()).map(|index| {
|
|
| 265 |
+ settings_app_id_row( |
|
| 266 |
+ &team.app_ids[index], |
|
| 267 |
+ index == selected_app_id, |
|
| 268 |
+ ) |
|
| 269 |
+ .id(("app-id-row", index))
|
|
| 270 |
+ .on_click(cx.listener( |
|
| 271 |
+ move |this, event, window, cx| {
|
|
| 272 |
+ this.select_app_id(index, event, window, cx) |
|
| 273 |
+ }, |
|
| 274 |
+ )) |
|
| 275 |
+ })) |
|
| 276 |
+ .id("app-id-options"),
|
|
| 277 |
+ )), |
|
| 278 |
+ ) |
|
| 279 |
+ }) |
|
| 280 |
+ .child(app_id_actions(!manual_enabled, cx)), |
|
| 281 |
+ ), |
|
| 282 |
+ ), |
|
| 283 |
+ ) |
|
| 284 |
+} |
|
| 285 |
+ |
|
| 286 |
+fn auto_app_id_checkbox(checked: bool, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 287 |
+ div() |
|
| 288 |
+ .id("auto-app-id")
|
|
| 289 |
+ .p_2() |
|
| 290 |
+ .rounded_md() |
|
| 291 |
+ .bg(rgb(0xf6f9f9)) |
|
| 292 |
+ .flex() |
|
| 293 |
+ .items_center() |
|
| 294 |
+ .gap_2() |
|
| 295 |
+ .cursor_pointer() |
|
| 296 |
+ .hover(|style| style.bg(rgb(0xebf1f0))) |
|
| 297 |
+ .on_click(cx.listener(SettingsWindow::toggle_auto_app_id)) |
|
| 298 |
+ .child( |
|
| 299 |
+ div() |
|
| 300 |
+ .flex_none() |
|
| 301 |
+ .w_4() |
|
| 302 |
+ .h_4() |
|
| 303 |
+ .rounded_sm() |
|
| 304 |
+ .border_1() |
|
| 305 |
+ .border_color(if checked {
|
|
| 306 |
+ rgb(0x173f45) |
|
| 307 |
+ } else {
|
|
| 308 |
+ rgb(0xaebcba) |
|
| 309 |
+ }) |
|
| 310 |
+ .bg(if checked {
|
|
| 311 |
+ rgb(0x173f45) |
|
| 312 |
+ } else {
|
|
| 313 |
+ rgb(0xffffff) |
|
| 314 |
+ }) |
|
| 315 |
+ .flex() |
|
| 316 |
+ .items_center() |
|
| 317 |
+ .justify_center() |
|
| 318 |
+ .when(checked, |this| {
|
|
| 319 |
+ this.child(div().w_2().h_2().rounded_sm().bg(rgb(0xffffff))) |
|
| 320 |
+ }), |
|
| 321 |
+ ) |
|
| 322 |
+ .child( |
|
| 323 |
+ div() |
|
| 324 |
+ .min_w_0() |
|
| 325 |
+ .flex() |
|
| 326 |
+ .flex_1() |
|
| 327 |
+ .flex_col() |
|
| 328 |
+ .gap_0p5() |
|
| 329 |
+ .child( |
|
| 330 |
+ div() |
|
| 331 |
+ .text_xs() |
|
| 332 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 333 |
+ .text_color(rgb(0x24333a)) |
|
| 334 |
+ .child("Automatically pick or create an App ID"),
|
|
| 335 |
+ ) |
|
| 336 |
+ .child( |
|
| 337 |
+ div() |
|
| 338 |
+ .text_xs() |
|
| 339 |
+ .text_color(rgb(0x6a7a81)) |
|
| 340 |
+ .child("Manual App ID selection stays visible below."),
|
|
| 341 |
+ ), |
|
| 342 |
+ ) |
|
| 343 |
+} |
|
| 344 |
+ |
|
| 345 |
+fn app_id_actions(disabled: bool, cx: &mut Context<SettingsWindow>) -> impl IntoElement {
|
|
| 346 |
+ div() |
|
| 347 |
+ .flex_none() |
|
| 348 |
+ .flex() |
|
| 349 |
+ .items_center() |
|
| 350 |
+ .gap_1() |
|
| 351 |
+ .child(app_id_icon_button( |
|
| 352 |
+ "add-app-id", |
|
| 353 |
+ "icons/plus.svg", |
|
| 354 |
+ disabled, |
|
| 355 |
+ cx.listener(SettingsWindow::add_app_id), |
|
| 356 |
+ )) |
|
| 357 |
+ .child(app_id_icon_button( |
|
| 358 |
+ "remove-app-id", |
|
| 359 |
+ "icons/minus.svg", |
|
| 360 |
+ disabled, |
|
| 361 |
+ cx.listener(SettingsWindow::remove_app_id), |
|
| 362 |
+ )) |
|
| 363 |
+ .child(app_id_icon_button( |
|
| 364 |
+ "edit-app-id", |
|
| 365 |
+ "icons/pencil.svg", |
|
| 366 |
+ disabled, |
|
| 367 |
+ cx.listener(SettingsWindow::edit_app_id), |
|
| 368 |
+ )) |
|
| 369 |
+} |
|
| 370 |
+ |
|
| 371 |
+fn app_id_icon_button( |
|
| 372 |
+ id: &'static str, |
|
| 373 |
+ icon: &'static str, |
|
| 374 |
+ disabled: bool, |
|
| 375 |
+ listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, |
|
| 376 |
+) -> impl IntoElement {
|
|
| 377 |
+ div() |
|
| 378 |
+ .flex_none() |
|
| 379 |
+ .w_6() |
|
| 380 |
+ .h_6() |
|
| 381 |
+ .rounded_md() |
|
| 382 |
+ .bg(rgb(0xebf1f0)) |
|
| 383 |
+ .text_color(rgb(0x53666d)) |
|
| 384 |
+ .flex() |
|
| 385 |
+ .items_center() |
|
| 386 |
+ .justify_center() |
|
| 387 |
+ .id(id) |
|
| 388 |
+ .when(disabled, |this| this.opacity(0.45)) |
|
| 389 |
+ .when(!disabled, |this| {
|
|
| 390 |
+ this.cursor_pointer() |
|
| 391 |
+ .hover(|style| style.bg(rgb(0xdfe8e6))) |
|
| 392 |
+ .on_click(listener) |
|
| 393 |
+ }) |
|
| 394 |
+ .child(lucide_icon(icon)) |
|
| 395 |
+} |
|
| 396 |
+ |
|
| 397 |
+fn settings_app_id_row(app_id: &AppIdOption, selected: bool) -> gpui::Div {
|
|
| 398 |
+ div() |
|
| 399 |
+ .h(px(COMBO_ITEM_HEIGHT)) |
|
| 400 |
+ .px_3() |
|
| 401 |
+ .rounded_md() |
|
| 402 |
+ .border_1() |
|
| 403 |
+ .border_color(if selected {
|
|
| 404 |
+ rgb(0x0f6f7a) |
|
| 405 |
+ } else {
|
|
| 406 |
+ rgb(0xd8e0df) |
|
| 407 |
+ }) |
|
| 408 |
+ .bg(if selected {
|
|
| 409 |
+ rgb(0xf0fbfa) |
|
| 410 |
+ } else {
|
|
| 411 |
+ rgb(0xffffff) |
|
| 412 |
+ }) |
|
| 413 |
+ .cursor_pointer() |
|
| 414 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 415 |
+ .flex() |
|
| 416 |
+ .items_center() |
|
| 417 |
+ .child(combo_item_content( |
|
| 418 |
+ app_id.identifier.clone(), |
|
| 419 |
+ app_id.name.clone(), |
|
| 420 |
+ app_id.kind.clone(), |
|
| 421 |
+ )) |
|
| 422 |
+} |
| ... | ... |
@@ -0,0 +1,163 @@ |
| 1 |
+use crate::models::MachineIdentity; |
|
| 2 |
+use std::env; |
|
| 3 |
+ |
|
| 4 |
+pub(crate) fn machine_identity() -> MachineIdentity {
|
|
| 5 |
+ platform_machine_identity().unwrap_or_else(fallback_machine_identity) |
|
| 6 |
+} |
|
| 7 |
+ |
|
| 8 |
+fn fallback_machine_identity() -> MachineIdentity {
|
|
| 9 |
+ let machine_name = env::var("HOSTNAME")
|
|
| 10 |
+ .or_else(|_| env::var("COMPUTERNAME"))
|
|
| 11 |
+ .unwrap_or_else(|_| "Local Machine".to_string()); |
|
| 12 |
+ let os_name = env::consts::OS.to_string(); |
|
| 13 |
+ let os_version = env::var("OS_VERSION").unwrap_or_else(|_| "Unknown".to_string());
|
|
| 14 |
+ let machine_id = env::var("MACHINE_ID")
|
|
| 15 |
+ .unwrap_or_else(|_| "A8B31C86-359B-4D95-8950-BA5DD8FFC46F".to_string()); |
|
| 16 |
+ |
|
| 17 |
+ MachineIdentity {
|
|
| 18 |
+ machine_name: machine_name.into(), |
|
| 19 |
+ os_name: os_name.into(), |
|
| 20 |
+ os_version: os_version.into(), |
|
| 21 |
+ machine_id: machine_id.into(), |
|
| 22 |
+ } |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 25 |
+#[cfg(target_os = "macos")] |
|
| 26 |
+fn platform_machine_identity() -> Option<MachineIdentity> {
|
|
| 27 |
+ let machine_name = macos::hardware_model()?; |
|
| 28 |
+ let (os_name, os_version) = macos::operating_system()?; |
|
| 29 |
+ let machine_id = macos::platform_uuid()?; |
|
| 30 |
+ |
|
| 31 |
+ Some(MachineIdentity {
|
|
| 32 |
+ machine_name: machine_name.into(), |
|
| 33 |
+ os_name: os_name.into(), |
|
| 34 |
+ os_version: os_version.into(), |
|
| 35 |
+ machine_id: machine_id.into(), |
|
| 36 |
+ }) |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+#[cfg(not(target_os = "macos"))] |
|
| 40 |
+fn platform_machine_identity() -> Option<MachineIdentity> {
|
|
| 41 |
+ None |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+#[cfg(target_os = "macos")] |
|
| 45 |
+mod macos {
|
|
| 46 |
+ use core_foundation::base::{kCFAllocatorDefault, CFAllocatorRef, CFTypeRef, TCFType};
|
|
| 47 |
+ use core_foundation::dictionary::{CFDictionaryRef, CFMutableDictionaryRef};
|
|
| 48 |
+ use core_foundation::string::{CFString, CFStringRef};
|
|
| 49 |
+ use libc::{c_char, c_void, size_t};
|
|
| 50 |
+ use plist::{Dictionary, Value};
|
|
| 51 |
+ use std::ffi::CString; |
|
| 52 |
+ use std::ptr; |
|
| 53 |
+ |
|
| 54 |
+ type KernReturn = i32; |
|
| 55 |
+ type MachPort = u32; |
|
| 56 |
+ type IoObject = u32; |
|
| 57 |
+ type IoRegistryEntry = IoObject; |
|
| 58 |
+ type IoService = IoObject; |
|
| 59 |
+ |
|
| 60 |
+ const K_IO_MAIN_PORT_DEFAULT: MachPort = 0; |
|
| 61 |
+ const K_IO_PLATFORM_EXPERT_DEVICE: &[u8] = b"IOPlatformExpertDevice\0"; |
|
| 62 |
+ const K_IO_PLATFORM_UUID: &str = "IOPlatformUUID"; |
|
| 63 |
+ const SYSTEM_VERSION_PLIST: &str = "/System/Library/CoreServices/SystemVersion.plist"; |
|
| 64 |
+ |
|
| 65 |
+ #[link(name = "IOKit", kind = "framework")] |
|
| 66 |
+ extern "C" {
|
|
| 67 |
+ fn IOServiceMatching(name: *const c_char) -> CFMutableDictionaryRef; |
|
| 68 |
+ fn IOServiceGetMatchingService(main_port: MachPort, matching: CFDictionaryRef) |
|
| 69 |
+ -> IoService; |
|
| 70 |
+ fn IORegistryEntryCreateCFProperty( |
|
| 71 |
+ entry: IoRegistryEntry, |
|
| 72 |
+ key: CFStringRef, |
|
| 73 |
+ allocator: CFAllocatorRef, |
|
| 74 |
+ options: u32, |
|
| 75 |
+ ) -> CFTypeRef; |
|
| 76 |
+ fn IOObjectRelease(object: IoObject) -> KernReturn; |
|
| 77 |
+ } |
|
| 78 |
+ |
|
| 79 |
+ pub(super) fn hardware_model() -> Option<String> {
|
|
| 80 |
+ sysctl_string("hw.model")
|
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ pub(super) fn operating_system() -> Option<(String, String)> {
|
|
| 84 |
+ let plist = plist::from_file::<_, Dictionary>(SYSTEM_VERSION_PLIST).ok()?; |
|
| 85 |
+ let product_name = plist_string(&plist, "ProductName").unwrap_or("macOS");
|
|
| 86 |
+ let product_version = plist_string(&plist, "ProductVersion")?; |
|
| 87 |
+ let build_version = plist_string(&plist, "ProductBuildVersion")?; |
|
| 88 |
+ |
|
| 89 |
+ Some(( |
|
| 90 |
+ product_name.to_string(), |
|
| 91 |
+ format!("{product_version};{build_version}"),
|
|
| 92 |
+ )) |
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ pub(super) fn platform_uuid() -> Option<String> {
|
|
| 96 |
+ let matching = |
|
| 97 |
+ unsafe { IOServiceMatching(K_IO_PLATFORM_EXPERT_DEVICE.as_ptr() as *const c_char) };
|
|
| 98 |
+ if matching.is_null() {
|
|
| 99 |
+ return None; |
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ let service = unsafe {
|
|
| 103 |
+ IOServiceGetMatchingService(K_IO_MAIN_PORT_DEFAULT, matching as CFDictionaryRef) |
|
| 104 |
+ }; |
|
| 105 |
+ if service == 0 {
|
|
| 106 |
+ return None; |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ let key = CFString::new(K_IO_PLATFORM_UUID); |
|
| 110 |
+ let value = unsafe {
|
|
| 111 |
+ IORegistryEntryCreateCFProperty( |
|
| 112 |
+ service, |
|
| 113 |
+ key.as_concrete_TypeRef(), |
|
| 114 |
+ kCFAllocatorDefault, |
|
| 115 |
+ 0, |
|
| 116 |
+ ) |
|
| 117 |
+ }; |
|
| 118 |
+ unsafe {
|
|
| 119 |
+ IOObjectRelease(service); |
|
| 120 |
+ } |
|
| 121 |
+ |
|
| 122 |
+ if value.is_null() {
|
|
| 123 |
+ return None; |
|
| 124 |
+ } |
|
| 125 |
+ |
|
| 126 |
+ let value = unsafe { CFString::wrap_under_create_rule(value as CFStringRef) };
|
|
| 127 |
+ Some(value.to_string()) |
|
| 128 |
+ } |
|
| 129 |
+ |
|
| 130 |
+ fn sysctl_string(name: &str) -> Option<String> {
|
|
| 131 |
+ let name = CString::new(name).ok()?; |
|
| 132 |
+ let mut len: size_t = 0; |
|
| 133 |
+ let len_result = unsafe {
|
|
| 134 |
+ libc::sysctlbyname(name.as_ptr(), ptr::null_mut(), &mut len, ptr::null_mut(), 0) |
|
| 135 |
+ }; |
|
| 136 |
+ if len_result != 0 || len == 0 {
|
|
| 137 |
+ return None; |
|
| 138 |
+ } |
|
| 139 |
+ |
|
| 140 |
+ let mut value = vec![0u8; len]; |
|
| 141 |
+ let result = unsafe {
|
|
| 142 |
+ libc::sysctlbyname( |
|
| 143 |
+ name.as_ptr(), |
|
| 144 |
+ value.as_mut_ptr() as *mut c_void, |
|
| 145 |
+ &mut len, |
|
| 146 |
+ ptr::null_mut(), |
|
| 147 |
+ 0, |
|
| 148 |
+ ) |
|
| 149 |
+ }; |
|
| 150 |
+ if result != 0 {
|
|
| 151 |
+ return None; |
|
| 152 |
+ } |
|
| 153 |
+ |
|
| 154 |
+ if value.last() == Some(&0) {
|
|
| 155 |
+ value.pop(); |
|
| 156 |
+ } |
|
| 157 |
+ String::from_utf8(value).ok() |
|
| 158 |
+ } |
|
| 159 |
+ |
|
| 160 |
+ fn plist_string<'a>(plist: &'a Dictionary, key: &str) -> Option<&'a str> {
|
|
| 161 |
+ plist.get(key).and_then(Value::as_string) |
|
| 162 |
+ } |
|
| 163 |
+} |
| ... | ... |
@@ -205,7 +205,7 @@ pub(crate) fn developer_account_title(role: &SharedString) -> SharedString {
|
| 205 | 205 |
} |
| 206 | 206 |
|
| 207 | 207 |
pub(crate) fn app_identity(app: &AppOption) -> SharedString {
|
| 208 |
- format!("{} - {} ({})", app.bundle_id, app.version, app.build).into()
|
|
| 208 |
+ format!("{} - {} ({})", app.bundle_id(), app.version(), app.build()).into()
|
|
| 209 | 209 |
} |
| 210 | 210 |
|
| 211 | 211 |
pub(crate) fn device_identity(device: &DeviceOption) -> SharedString {
|
| 212 | 212 |