Align desktop utility panels
This commit is contained in:
@@ -3715,6 +3715,7 @@ enum Icon {
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Close,
|
||||
Command,
|
||||
Copy,
|
||||
Delete,
|
||||
@@ -3773,6 +3774,12 @@ impl<Message> canvas::Program<Message> for IconCanvas {
|
||||
path.line_to(Point::new(10.5, 8.0));
|
||||
path.line_to(Point::new(5.5, 13.0));
|
||||
}
|
||||
Icon::Close => {
|
||||
path.move_to(Point::new(3.0, 3.0));
|
||||
path.line_to(Point::new(13.0, 13.0));
|
||||
path.move_to(Point::new(13.0, 3.0));
|
||||
path.line_to(Point::new(3.0, 13.0));
|
||||
}
|
||||
Icon::Command => {
|
||||
path.move_to(Point::new(2.0, 4.0));
|
||||
path.line_to(Point::new(6.0, 8.0));
|
||||
@@ -3985,6 +3992,37 @@ fn entry_icon_control(icon: Icon, hint: String, message: Message) -> Element<'st
|
||||
.into()
|
||||
}
|
||||
|
||||
fn utility_icon_control(
|
||||
icon: Icon,
|
||||
hint: &'static str,
|
||||
message: Message,
|
||||
enabled: bool,
|
||||
) -> Element<'static, Message> {
|
||||
let control = button(colored_icon_view(icon, Some(ENTRY_TEXT)))
|
||||
.padding(6)
|
||||
.style(entry_icon_button);
|
||||
tooltip(
|
||||
if enabled {
|
||||
control.on_press(message)
|
||||
} else {
|
||||
control
|
||||
},
|
||||
container(text(hint).size(12))
|
||||
.padding([5, 8])
|
||||
.style(container::dark),
|
||||
tooltip::Position::Bottom,
|
||||
)
|
||||
.gap(4)
|
||||
.delay(Duration::from_millis(350))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn utility_button<'a>(
|
||||
content: impl Into<Element<'a, Message>>,
|
||||
) -> iced::widget::Button<'a, Message> {
|
||||
button(content).padding([6, 10]).style(entry_icon_button)
|
||||
}
|
||||
|
||||
fn entry_icon_button(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: matches!(status, button::Status::Hovered | button::Status::Pressed)
|
||||
@@ -4148,12 +4186,54 @@ fn platform_menu_bar(app: &App) -> Element<'_, Message> {
|
||||
container(menu).width(Length::Fill).into()
|
||||
}
|
||||
|
||||
fn utility_title(utility: &UtilityView) -> &'static str {
|
||||
match utility {
|
||||
UtilityView::About => "About IronStorage",
|
||||
UtilityView::Settings(_) => "Settings",
|
||||
UtilityView::Recipients(form) => match form.kind {
|
||||
RecipientWorkflowKind::InitializeStore => "Initialize Password Store",
|
||||
RecipientWorkflowKind::NewFolder => "Create Password Store Folder",
|
||||
},
|
||||
UtilityView::NewEntry(_) => "New Entry",
|
||||
UtilityView::Search(form) => match form.mode {
|
||||
SearchMode::Names => "Find Entries and Folders",
|
||||
SearchMode::Contents => "Search Decrypted Contents",
|
||||
},
|
||||
UtilityView::Mutation(form) => match form.kind {
|
||||
MutationKind::Move => "Move or Rename",
|
||||
MutationKind::Copy => "Copy Entry or Folder",
|
||||
MutationKind::Delete => "Delete Entry or Folder",
|
||||
},
|
||||
UtilityView::Git(_) => "Git Synchronization",
|
||||
UtilityView::Otp(_) => "One-Time Password",
|
||||
UtilityView::Kdbx(_) => "Import KeePass Database",
|
||||
UtilityView::Help => "IronStorage Help",
|
||||
}
|
||||
}
|
||||
|
||||
fn utility_icon(utility: &UtilityView) -> Icon {
|
||||
match utility {
|
||||
UtilityView::About | UtilityView::Help => Icon::Key,
|
||||
UtilityView::Settings(_) => Icon::Settings,
|
||||
UtilityView::Recipients(_) => Icon::Key,
|
||||
UtilityView::NewEntry(_) => Icon::Add,
|
||||
UtilityView::Search(_) => Icon::Search,
|
||||
UtilityView::Mutation(form) => match form.kind {
|
||||
MutationKind::Move => Icon::Move,
|
||||
MutationKind::Copy => Icon::Copy,
|
||||
MutationKind::Delete => Icon::Delete,
|
||||
},
|
||||
UtilityView::Git(_) => Icon::Refresh,
|
||||
UtilityView::Otp(_) => Icon::Generate,
|
||||
UtilityView::Kdbx(_) => Icon::Down,
|
||||
}
|
||||
}
|
||||
|
||||
fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Message> {
|
||||
let mut content = column![].spacing(10);
|
||||
match utility {
|
||||
UtilityView::About => {
|
||||
content = content
|
||||
.push(text(ironstorage::PRODUCT_NAME).size(28))
|
||||
.push(text(format!("Version {}", env!("CARGO_PKG_VERSION"))))
|
||||
.push(text("A native, pass-compatible password-store client."))
|
||||
.push(text("Compatible with pass and pass-otp."))
|
||||
@@ -4163,73 +4243,101 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
));
|
||||
}
|
||||
UtilityView::Settings(form) => {
|
||||
content = content
|
||||
.push(text("Settings").size(28))
|
||||
.push(text("Password-store folder"))
|
||||
.push(
|
||||
row![
|
||||
text_input("Vault path", &form.vault)
|
||||
.id(settings_vault_id())
|
||||
.on_input(Message::SettingsVaultChanged)
|
||||
.on_submit(Message::SaveSettings),
|
||||
if form.saving {
|
||||
button("Choose…")
|
||||
} else {
|
||||
button("Choose…").on_press(Message::PickSettingsVault)
|
||||
},
|
||||
]
|
||||
.spacing(8),
|
||||
)
|
||||
.push(text("Default OpenPGP key fingerprint or identity"))
|
||||
.push(
|
||||
text_input("Default key", &form.default_key)
|
||||
.on_input(Message::SettingsDefaultKeyChanged)
|
||||
.on_submit(Message::SaveSettings),
|
||||
)
|
||||
.push(text(
|
||||
"Authentication inactivity timeout in seconds (1–86400)",
|
||||
))
|
||||
.push(
|
||||
text_input("Timeout seconds", &form.authentication_timeout)
|
||||
.on_input(Message::SettingsTimeoutChanged)
|
||||
.on_submit(Message::SaveSettings),
|
||||
)
|
||||
.push(text("Command-line tools"))
|
||||
.push(text(
|
||||
let general = column![
|
||||
text("General").size(18),
|
||||
text("Password-store folder").size(13),
|
||||
row![
|
||||
text_input("Vault path", &form.vault)
|
||||
.id(settings_vault_id())
|
||||
.on_input(Message::SettingsVaultChanged)
|
||||
.on_submit(Message::SaveSettings)
|
||||
.style(entry_input_style),
|
||||
utility_icon_control(
|
||||
Icon::Folder,
|
||||
"Choose password-store folder",
|
||||
Message::PickSettingsVault,
|
||||
!form.saving,
|
||||
),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(6),
|
||||
text("Default OpenPGP key fingerprint or identity").size(13),
|
||||
text_input("Default key", &form.default_key)
|
||||
.on_input(Message::SettingsDefaultKeyChanged)
|
||||
.on_submit(Message::SaveSettings)
|
||||
.style(entry_input_style),
|
||||
text("Authentication inactivity timeout in seconds (1–86400)").size(13),
|
||||
text_input("Timeout seconds", &form.authentication_timeout)
|
||||
.on_input(Message::SettingsTimeoutChanged)
|
||||
.on_submit(Message::SaveSettings)
|
||||
.style(entry_input_style),
|
||||
]
|
||||
.spacing(8);
|
||||
content = content.push(
|
||||
container(general)
|
||||
.padding(14)
|
||||
.width(Length::Fill)
|
||||
.style(|theme| entry_field_style(theme, false)),
|
||||
);
|
||||
|
||||
let mut commands = column![
|
||||
row![
|
||||
text("Command-line tools").size(18).width(Length::Fill),
|
||||
utility_icon_control(
|
||||
Icon::Link,
|
||||
"Install CLI and TUI links",
|
||||
Message::InstallCommandLinks,
|
||||
!form.saving,
|
||||
),
|
||||
]
|
||||
.align_y(iced::Alignment::Center),
|
||||
text(
|
||||
"Install links to the CLI and TUI embedded in this application bundle in ~/.local/bin. Existing command files or links are replaced.",
|
||||
))
|
||||
.push(if form.saving {
|
||||
button("Install CLI and TUI Links")
|
||||
} else {
|
||||
button("Install CLI and TUI Links").on_press(Message::InstallCommandLinks)
|
||||
});
|
||||
)
|
||||
.size(13),
|
||||
]
|
||||
.spacing(8);
|
||||
if let Some(status) = &form.command_links_status {
|
||||
content = content.push(text(status));
|
||||
commands = commands.push(text(status).size(13));
|
||||
}
|
||||
content = content.push(
|
||||
container(commands)
|
||||
.padding(14)
|
||||
.width(Length::Fill)
|
||||
.style(|theme| entry_field_style(theme, false)),
|
||||
);
|
||||
if let Some(storage) = &app.storage {
|
||||
content = content
|
||||
.push(text(format!(
|
||||
content = content.push(
|
||||
container(
|
||||
column![
|
||||
text("Configuration details").size(18),
|
||||
text(format!(
|
||||
"Shared configuration: {}",
|
||||
storage.config_source().display()
|
||||
)))
|
||||
.push(text(format!(
|
||||
"Clipboard cleanup remains {} seconds.",
|
||||
storage.clipboard_timeout().duration().as_secs()
|
||||
)))
|
||||
.push(text(
|
||||
"Save validates the repository, key material, entry tree, and authentication session before atomically replacing config.toml.",
|
||||
));
|
||||
))
|
||||
.size(13),
|
||||
text(format!(
|
||||
"Clipboard cleanup remains {} seconds.",
|
||||
storage.clipboard_timeout().duration().as_secs()
|
||||
))
|
||||
.size(13),
|
||||
text(
|
||||
"Save validates the repository, key material, entry tree, and authentication session before atomically replacing config.toml.",
|
||||
)
|
||||
.size(13),
|
||||
]
|
||||
.spacing(6),
|
||||
)
|
||||
.padding(14)
|
||||
.width(Length::Fill)
|
||||
.style(entry_value_style),
|
||||
);
|
||||
}
|
||||
if let Some(error) = &form.error {
|
||||
content = content.push(text(format!("Settings error: {error}")));
|
||||
}
|
||||
}
|
||||
UtilityView::Recipients(form) => {
|
||||
let title = match form.kind {
|
||||
RecipientWorkflowKind::InitializeStore => "Initialize Password Store",
|
||||
RecipientWorkflowKind::NewFolder => "Create Password Store Folder",
|
||||
};
|
||||
content = content.push(text(title).size(28));
|
||||
match form.kind {
|
||||
RecipientWorkflowKind::InitializeStore => {
|
||||
content = content.push(text(
|
||||
@@ -4242,7 +4350,8 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
.push(
|
||||
text_input("team/services", &form.path)
|
||||
.on_input(Message::RecipientPathChanged)
|
||||
.on_submit(Message::SubmitRecipient),
|
||||
.on_submit(Message::SubmitRecipient)
|
||||
.style(entry_input_style),
|
||||
)
|
||||
.push(text(
|
||||
"An upstream-compatible nested .gpg-id persists the folder and controls its recipients.",
|
||||
@@ -4256,7 +4365,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let choice = button(text(format!(
|
||||
let choice = utility_button(text(format!(
|
||||
"[{}] {}{}",
|
||||
if recipient.selected { "x" } else { " " },
|
||||
recipient.label,
|
||||
@@ -4269,7 +4378,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
};
|
||||
if form.kind == RecipientWorkflowKind::InitializeStore {
|
||||
let selected_default = recipient.fingerprint == form.default_key;
|
||||
let default = button(text(if !recipient.can_default {
|
||||
let default = utility_button(text(if !recipient.can_default {
|
||||
"Public recipient only"
|
||||
} else if selected_default {
|
||||
"Default key ✓"
|
||||
@@ -4286,7 +4395,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
content = content.push(choice);
|
||||
}
|
||||
}
|
||||
let confirmation = button(text(format!(
|
||||
let confirmation = utility_button(text(format!(
|
||||
"[{}] Replace this recipient policy and selectively re-encrypt affected entries",
|
||||
if form.confirmed { "x" } else { " " }
|
||||
)));
|
||||
@@ -4300,15 +4409,13 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
}
|
||||
}
|
||||
UtilityView::NewEntry(form) => {
|
||||
content = content
|
||||
.push(text("New Entry").size(28))
|
||||
.push(text("Entry path"))
|
||||
.push(
|
||||
text_input("folder/account", &form.path)
|
||||
.on_input(Message::NewEntryPathChanged)
|
||||
.on_submit(Message::SubmitNewEntry),
|
||||
);
|
||||
let generate = button(text(format!(
|
||||
content = content.push(text("Entry path")).push(
|
||||
text_input("folder/account", &form.path)
|
||||
.on_input(Message::NewEntryPathChanged)
|
||||
.on_submit(Message::SubmitNewEntry)
|
||||
.style(entry_input_style),
|
||||
);
|
||||
let generate = utility_button(text(format!(
|
||||
"[{}] Generate the initial password with storage policy",
|
||||
if form.generate { "x" } else { " " }
|
||||
)));
|
||||
@@ -4321,9 +4428,10 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
content = content.push(text("Password length")).push(
|
||||
text_input("25", &form.length)
|
||||
.on_input(Message::NewEntryLengthChanged)
|
||||
.on_submit(Message::SubmitNewEntry),
|
||||
.on_submit(Message::SubmitNewEntry)
|
||||
.style(entry_input_style),
|
||||
);
|
||||
let symbols = button(text(format!(
|
||||
let symbols = utility_button(text(format!(
|
||||
"[{}] Letters and digits only",
|
||||
if form.no_symbols { "x" } else { " " }
|
||||
)));
|
||||
@@ -4341,11 +4449,6 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
}
|
||||
}
|
||||
UtilityView::Search(form) => {
|
||||
let title = match form.mode {
|
||||
SearchMode::Names => "Find Entries and Folders",
|
||||
SearchMode::Contents => "Search Decrypted Contents",
|
||||
};
|
||||
content = content.push(text(title).size(28));
|
||||
let input = text_input(
|
||||
if form.mode == SearchMode::Names {
|
||||
"Name substring"
|
||||
@@ -4355,9 +4458,10 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
&form.query,
|
||||
)
|
||||
.on_input(Message::SearchQueryChanged)
|
||||
.on_submit(Message::SubmitSearch);
|
||||
.on_submit(Message::SubmitSearch)
|
||||
.style(entry_input_style);
|
||||
content = if form.mode == SearchMode::Names {
|
||||
let add = button("Add another term");
|
||||
let add = utility_button("Add another term");
|
||||
content.push(
|
||||
row![
|
||||
input,
|
||||
@@ -4391,7 +4495,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
"Name search remains available while locked. Multiple terms use the storage crate's pass-compatible OR matching semantics.",
|
||||
));
|
||||
for (index, term) in form.terms.iter().enumerate() {
|
||||
let remove = button(text(format!("Remove term · {term}")));
|
||||
let remove = utility_button(text(format!("Remove term · {term}")));
|
||||
content = content.push(if form.running {
|
||||
remove
|
||||
} else {
|
||||
@@ -4416,7 +4520,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
"Entry"
|
||||
};
|
||||
content = content.push(
|
||||
button(text(format!("{kind} · {}", matched.path())))
|
||||
utility_button(text(format!("{kind} · {}", matched.path())))
|
||||
.on_press(Message::ActivateSearchResult(matched.id().clone())),
|
||||
);
|
||||
}
|
||||
@@ -4427,7 +4531,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
}
|
||||
for entry in results.entries() {
|
||||
content = content.push(
|
||||
button(text(format!("Entry · {}", entry.path()))).on_press(
|
||||
utility_button(text(format!("Entry · {}", entry.path()))).on_press(
|
||||
Message::ActivateSearchResult(TreeNodeId::Entry(
|
||||
entry.path().clone(),
|
||||
)),
|
||||
@@ -4450,35 +4554,29 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
}
|
||||
}
|
||||
UtilityView::Mutation(form) => {
|
||||
let title = match form.kind {
|
||||
MutationKind::Move => "Move or Rename",
|
||||
MutationKind::Copy => "Copy Entry or Folder",
|
||||
MutationKind::Delete => "Delete Entry or Folder",
|
||||
};
|
||||
content = content
|
||||
.push(text(title).size(28))
|
||||
.push(text(format!("Source: {}", form.source.path().display())));
|
||||
content = content.push(text(format!("Source: {}", form.source.path().display())));
|
||||
if form.kind != MutationKind::Delete {
|
||||
content = content
|
||||
.push(text("Destination entry path or existing folder"))
|
||||
.push(
|
||||
text_input("folder/name", &form.destination)
|
||||
.on_input(Message::MutationDestinationChanged)
|
||||
.on_submit(Message::SubmitMutation),
|
||||
.on_submit(Message::SubmitMutation)
|
||||
.style(entry_input_style),
|
||||
)
|
||||
.push(text("Choose an existing destination folder"));
|
||||
let mut destinations = row![].spacing(6);
|
||||
for directory in app.navigation.directories() {
|
||||
let path = directory.path().display().to_string();
|
||||
let label = if path.is_empty() { "Store root" } else { &path };
|
||||
let choice = button(text(label.to_owned()));
|
||||
let choice = utility_button(text(label.to_owned()));
|
||||
destinations = destinations.push(if form.running {
|
||||
choice
|
||||
} else {
|
||||
choice.on_press(Message::SelectMutationDestination(path))
|
||||
});
|
||||
}
|
||||
let overwrite = button(text(format!(
|
||||
let overwrite = utility_button(text(format!(
|
||||
"[{}] Replace an existing destination entry",
|
||||
if form.overwrite { "x" } else { " " }
|
||||
)));
|
||||
@@ -4488,7 +4586,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
overwrite.on_press(Message::ToggleMutationOverwrite)
|
||||
});
|
||||
} else {
|
||||
let confirmation = button(text(format!(
|
||||
let confirmation = utility_button(text(format!(
|
||||
"[{}] Permanently remove this {} and commit the deletion",
|
||||
if form.confirmed { "x" } else { " " },
|
||||
if form.source.is_directory() {
|
||||
@@ -4511,11 +4609,9 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
}
|
||||
}
|
||||
UtilityView::Git(form) => {
|
||||
content = content
|
||||
.push(text("Git Synchronization").size(28))
|
||||
.push(text(
|
||||
"All repository, HTTPS transport, credential, merge, and conflict decisions are owned by crates/storage. No git process or credential helper is launched.",
|
||||
));
|
||||
content = content.push(text(
|
||||
"All repository, HTTPS transport, credential, merge, and conflict decisions are owned by crates/storage. No git process or credential helper is launched.",
|
||||
));
|
||||
if let Some(phase) = form.progress {
|
||||
content = content.push(text(format!("Progress: {}", git_phase_name(phase))));
|
||||
}
|
||||
@@ -4579,14 +4675,14 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
"Choose exactly one complete version for every path. Resolution is committed by crates/storage only after all choices are present.",
|
||||
));
|
||||
for (index, selection) in form.conflicts.iter().enumerate() {
|
||||
let local = button(text(
|
||||
let local = utility_button(text(
|
||||
if selection.choice == Some(GitConflictChoice::Local) {
|
||||
"Local ✓"
|
||||
} else {
|
||||
"Use local"
|
||||
},
|
||||
));
|
||||
let remote = button(text(
|
||||
let remote = utility_button(text(
|
||||
if selection.choice == Some(GitConflictChoice::Remote) {
|
||||
"Remote ✓"
|
||||
} else {
|
||||
@@ -4619,14 +4715,14 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
}
|
||||
UtilityView::Otp(form) => {
|
||||
content = content
|
||||
.push(text("One-Time Password").size(28))
|
||||
.push(text(
|
||||
"Code generation, counters, URI validation, QR payloads, entry mutation, Git commits, and clipboard policy are owned by crates/storage.",
|
||||
))
|
||||
.push(text("Entry path"))
|
||||
.push(
|
||||
text_input("folder/entry", &form.entry)
|
||||
.on_input(Message::OtpEntryChanged),
|
||||
.on_input(Message::OtpEntryChanged)
|
||||
.style(entry_input_style),
|
||||
);
|
||||
if let Some(display) = &app.sensitive.otp {
|
||||
content = content
|
||||
@@ -4676,7 +4772,8 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
.push(text("Import or replace OTP").size(20))
|
||||
.push(
|
||||
text_input("otpauth://…", &form.uri)
|
||||
.on_input(|value| Message::OtpUriChanged(Zeroizing::new(value))),
|
||||
.on_input(|value| Message::OtpUriChanged(Zeroizing::new(value)))
|
||||
.style(entry_input_style),
|
||||
)
|
||||
.push(search_option(
|
||||
"Replace an existing OTP URI",
|
||||
@@ -4701,7 +4798,6 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
}
|
||||
UtilityView::Kdbx(form) => {
|
||||
content = content
|
||||
.push(text("Import KeePass Database").size(28))
|
||||
.push(text(
|
||||
"The import is additive: full mode adds new entries and updates changed entries; quick-add mode only adds entries that do not exist. Nothing is deleted.",
|
||||
))
|
||||
@@ -4710,11 +4806,12 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
row![
|
||||
text_input("Database.kdbx", &form.source)
|
||||
.on_input(Message::KdbxSourceChanged)
|
||||
.on_submit(Message::SubmitKdbxImport),
|
||||
.on_submit(Message::SubmitKdbxImport)
|
||||
.style(entry_input_style),
|
||||
if form.running {
|
||||
button("Choose…")
|
||||
utility_button("Choose…")
|
||||
} else {
|
||||
button("Choose…").on_press(Message::PickKdbxSource)
|
||||
utility_button("Choose…").on_press(Message::PickKdbxSource)
|
||||
},
|
||||
]
|
||||
.spacing(8),
|
||||
@@ -4724,11 +4821,12 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
row![
|
||||
text_input("No key file", &form.key_file)
|
||||
.on_input(Message::KdbxKeyFileChanged)
|
||||
.on_submit(Message::SubmitKdbxImport),
|
||||
.on_submit(Message::SubmitKdbxImport)
|
||||
.style(entry_input_style),
|
||||
if form.running {
|
||||
button("Choose…")
|
||||
utility_button("Choose…")
|
||||
} else {
|
||||
button("Choose…").on_press(Message::PickKdbxKeyFile)
|
||||
utility_button("Choose…").on_press(Message::PickKdbxKeyFile)
|
||||
},
|
||||
]
|
||||
.spacing(8),
|
||||
@@ -4738,7 +4836,8 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
text_input("Password", &form.password)
|
||||
.secure(true)
|
||||
.on_input(|value| Message::KdbxPasswordChanged(Zeroizing::new(value)))
|
||||
.on_submit(Message::SubmitKdbxImport),
|
||||
.on_submit(Message::SubmitKdbxImport)
|
||||
.style(entry_input_style),
|
||||
)
|
||||
.push(search_option(
|
||||
"Quick add: only add entries not already present",
|
||||
@@ -4760,9 +4859,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
}
|
||||
}
|
||||
UtilityView::Help => {
|
||||
content = content
|
||||
.push(text("IronStorage Help").size(28))
|
||||
.push(text("Keyboard shortcuts").size(22));
|
||||
content = content.push(text("Keyboard shortcuts").size(22));
|
||||
for spec in action::ACTIONS {
|
||||
if let Some(shortcut) = action::shortcut_label(spec.action) {
|
||||
content = content.push(text(format!(
|
||||
@@ -4795,7 +4892,6 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
));
|
||||
}
|
||||
}
|
||||
let done = button("Done (Esc)");
|
||||
let busy = matches!(utility, UtilityView::Settings(form) if form.saving)
|
||||
|| matches!(utility, UtilityView::Recipients(form) if form.running)
|
||||
|| matches!(utility, UtilityView::NewEntry(form) if form.running)
|
||||
@@ -4804,230 +4900,246 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
||||
|| matches!(utility, UtilityView::Git(form) if form.running)
|
||||
|| matches!(utility, UtilityView::Otp(form) if form.running)
|
||||
|| matches!(utility, UtilityView::Kdbx(form) if form.running);
|
||||
let done = if busy {
|
||||
done
|
||||
} else {
|
||||
done.on_press(Message::DismissUtility)
|
||||
};
|
||||
let actions = match utility {
|
||||
UtilityView::Settings(form) => {
|
||||
let save = button(if form.saving {
|
||||
"Validating…"
|
||||
UtilityView::Settings(form) => row![utility_icon_control(
|
||||
Icon::Check,
|
||||
if form.saving {
|
||||
"Validating settings…"
|
||||
} else {
|
||||
"Save Settings"
|
||||
});
|
||||
row![
|
||||
if form.saving {
|
||||
save
|
||||
} else {
|
||||
save.on_press(Message::SaveSettings)
|
||||
},
|
||||
done,
|
||||
]
|
||||
.spacing(8)
|
||||
}
|
||||
UtilityView::Recipients(form) => {
|
||||
let apply = button(if form.running {
|
||||
"Applying…"
|
||||
"Save settings"
|
||||
},
|
||||
Message::SaveSettings,
|
||||
!form.saving,
|
||||
)],
|
||||
UtilityView::Recipients(form) => row![utility_icon_control(
|
||||
Icon::Check,
|
||||
if form.running {
|
||||
"Applying recipient policy…"
|
||||
} else {
|
||||
"Apply Recipient Policy"
|
||||
});
|
||||
row![
|
||||
if form.running {
|
||||
apply
|
||||
} else {
|
||||
apply.on_press(Message::SubmitRecipient)
|
||||
},
|
||||
done,
|
||||
]
|
||||
.spacing(8)
|
||||
}
|
||||
UtilityView::NewEntry(form) => {
|
||||
let create = button(if form.running {
|
||||
"Preparing…"
|
||||
"Apply recipient policy"
|
||||
},
|
||||
Message::SubmitRecipient,
|
||||
!form.running,
|
||||
)],
|
||||
UtilityView::NewEntry(form) => row![utility_icon_control(
|
||||
Icon::Check,
|
||||
if form.running {
|
||||
"Preparing draft…"
|
||||
} else {
|
||||
"Create Draft"
|
||||
});
|
||||
row![
|
||||
if form.running {
|
||||
create
|
||||
} else {
|
||||
create.on_press(Message::SubmitNewEntry)
|
||||
},
|
||||
done,
|
||||
]
|
||||
.spacing(8)
|
||||
}
|
||||
UtilityView::Search(form) => {
|
||||
let submit = button(if form.running {
|
||||
"Create draft"
|
||||
},
|
||||
Message::SubmitNewEntry,
|
||||
!form.running,
|
||||
)],
|
||||
UtilityView::Search(form) => row![utility_icon_control(
|
||||
Icon::Search,
|
||||
if form.running {
|
||||
"Searching…"
|
||||
} else {
|
||||
"Search"
|
||||
});
|
||||
row![
|
||||
if form.running {
|
||||
submit
|
||||
} else {
|
||||
submit.on_press(Message::SubmitSearch)
|
||||
},
|
||||
done,
|
||||
]
|
||||
.spacing(8)
|
||||
}
|
||||
},
|
||||
Message::SubmitSearch,
|
||||
!form.running,
|
||||
)],
|
||||
UtilityView::Mutation(form) => {
|
||||
let submit = button(if form.running {
|
||||
"Applying…"
|
||||
} else {
|
||||
match form.kind {
|
||||
MutationKind::Move => "Move",
|
||||
MutationKind::Copy => "Copy",
|
||||
MutationKind::Delete => "Delete",
|
||||
}
|
||||
});
|
||||
row![
|
||||
let icon = match form.kind {
|
||||
MutationKind::Move => Icon::Move,
|
||||
MutationKind::Copy => Icon::Copy,
|
||||
MutationKind::Delete => Icon::Delete,
|
||||
};
|
||||
row![utility_icon_control(
|
||||
icon,
|
||||
if form.running {
|
||||
submit
|
||||
"Applying mutation…"
|
||||
} else {
|
||||
submit.on_press(Message::SubmitMutation)
|
||||
"Apply mutation"
|
||||
},
|
||||
done,
|
||||
]
|
||||
.spacing(8)
|
||||
Message::SubmitMutation,
|
||||
!form.running,
|
||||
)]
|
||||
}
|
||||
UtilityView::Git(form) => {
|
||||
if form.running {
|
||||
row![
|
||||
button("Cancel Git operation").on_press(Message::CancelGit),
|
||||
done
|
||||
]
|
||||
.spacing(8)
|
||||
row![utility_button("Cancel Git operation").on_press(Message::CancelGit)]
|
||||
} else {
|
||||
let mut actions = row![
|
||||
icon_control(
|
||||
utility_icon_control(
|
||||
Icon::Refresh,
|
||||
"Refresh Git status".to_owned(),
|
||||
Message::RunGit(DesktopGitRequest::Refresh)
|
||||
"Refresh Git status",
|
||||
Message::RunGit(DesktopGitRequest::Refresh),
|
||||
true,
|
||||
),
|
||||
icon_control(
|
||||
utility_icon_control(
|
||||
Icon::Down,
|
||||
"Pull from remote".to_owned(),
|
||||
Message::RunGit(DesktopGitRequest::Pull)
|
||||
"Pull from remote",
|
||||
Message::RunGit(DesktopGitRequest::Pull),
|
||||
true,
|
||||
),
|
||||
icon_control(
|
||||
utility_icon_control(
|
||||
Icon::Up,
|
||||
"Push to remote".to_owned(),
|
||||
Message::RunGit(DesktopGitRequest::Push)
|
||||
"Push to remote",
|
||||
Message::RunGit(DesktopGitRequest::Push),
|
||||
true,
|
||||
),
|
||||
icon_control(
|
||||
utility_icon_control(
|
||||
Icon::Refresh,
|
||||
"Synchronize with remote".to_owned(),
|
||||
Message::RunGit(DesktopGitRequest::Sync)
|
||||
"Synchronize with remote",
|
||||
Message::RunGit(DesktopGitRequest::Sync),
|
||||
true,
|
||||
),
|
||||
]
|
||||
.spacing(2);
|
||||
if !form.conflicts.is_empty() {
|
||||
actions = actions.push(icon_control(
|
||||
actions = actions.push(utility_icon_control(
|
||||
Icon::Check,
|
||||
"Resolve selected versions".to_owned(),
|
||||
"Resolve selected versions",
|
||||
Message::ResolveGitConflicts,
|
||||
true,
|
||||
));
|
||||
}
|
||||
actions.push(done)
|
||||
actions
|
||||
}
|
||||
}
|
||||
UtilityView::Otp(form) => {
|
||||
if form.running {
|
||||
row![button("Working…"), done].spacing(8)
|
||||
row![utility_icon_control(
|
||||
Icon::Refresh,
|
||||
"Working…",
|
||||
Message::RunOtpCode(false),
|
||||
false,
|
||||
)]
|
||||
} else {
|
||||
let mut actions = row![
|
||||
icon_control(
|
||||
utility_icon_control(
|
||||
Icon::Refresh,
|
||||
"Generate OTP code".to_owned(),
|
||||
Message::RunOtpCode(false)
|
||||
"Generate OTP code",
|
||||
Message::RunOtpCode(false),
|
||||
true,
|
||||
),
|
||||
icon_control(
|
||||
utility_icon_control(
|
||||
Icon::Copy,
|
||||
"Copy OTP code".to_owned(),
|
||||
Message::RunOtpCode(true)
|
||||
"Copy OTP code",
|
||||
Message::RunOtpCode(true),
|
||||
true,
|
||||
),
|
||||
icon_control(
|
||||
utility_icon_control(
|
||||
Icon::Link,
|
||||
"Show provisioning URI".to_owned(),
|
||||
"Show provisioning URI",
|
||||
Message::RunOtpUri {
|
||||
qr: false,
|
||||
copy: false,
|
||||
}
|
||||
},
|
||||
true,
|
||||
),
|
||||
icon_control(
|
||||
utility_icon_control(
|
||||
Icon::Copy,
|
||||
"Copy provisioning URI".to_owned(),
|
||||
"Copy provisioning URI",
|
||||
Message::RunOtpUri {
|
||||
qr: false,
|
||||
copy: true,
|
||||
}
|
||||
},
|
||||
true,
|
||||
),
|
||||
icon_control(
|
||||
utility_icon_control(
|
||||
Icon::Qr,
|
||||
"Show provisioning QR code".to_owned(),
|
||||
"Show provisioning QR code",
|
||||
Message::RunOtpUri {
|
||||
qr: true,
|
||||
copy: false,
|
||||
}
|
||||
},
|
||||
true,
|
||||
),
|
||||
]
|
||||
.spacing(2);
|
||||
if form.hotp_confirmation {
|
||||
actions = actions.push(
|
||||
button("Confirm HOTP counter advance").on_press(Message::ConfirmHotp),
|
||||
utility_button("Confirm HOTP counter advance")
|
||||
.on_press(Message::ConfirmHotp),
|
||||
);
|
||||
}
|
||||
actions
|
||||
.push(icon_control(
|
||||
.push(utility_icon_control(
|
||||
Icon::Down,
|
||||
"Import provisioning URI".to_owned(),
|
||||
"Import provisioning URI",
|
||||
Message::SubmitOtpImport,
|
||||
true,
|
||||
))
|
||||
.push(icon_control(
|
||||
.push(utility_icon_control(
|
||||
Icon::Qr,
|
||||
"Import provisioning QR image".to_owned(),
|
||||
"Import provisioning QR image",
|
||||
Message::PickOtpQr,
|
||||
true,
|
||||
))
|
||||
.push(
|
||||
button("Remove OTP")
|
||||
utility_button("Remove OTP")
|
||||
.style(button::danger)
|
||||
.on_press(Message::SubmitOtpRemoval),
|
||||
)
|
||||
.push(done)
|
||||
}
|
||||
}
|
||||
UtilityView::Kdbx(form) => {
|
||||
let import = button(if form.running {
|
||||
UtilityView::Kdbx(form) => row![utility_icon_control(
|
||||
Icon::Down,
|
||||
if form.running {
|
||||
"Importing…"
|
||||
} else if form.quick_add {
|
||||
"Quick Add"
|
||||
"Quick add KeePass entries"
|
||||
} else {
|
||||
"Import"
|
||||
});
|
||||
row![
|
||||
if form.running {
|
||||
import
|
||||
} else {
|
||||
import.on_press(Message::SubmitKdbxImport)
|
||||
},
|
||||
done,
|
||||
]
|
||||
.spacing(8)
|
||||
}
|
||||
UtilityView::About | UtilityView::Help => row![done],
|
||||
"Import KeePass entries"
|
||||
},
|
||||
Message::SubmitKdbxImport,
|
||||
!form.running,
|
||||
)],
|
||||
UtilityView::About | UtilityView::Help => row![container(text("")).width(Length::Fill)],
|
||||
};
|
||||
container(
|
||||
column![scrollable(content).height(Length::Fill), actions,]
|
||||
.spacing(12)
|
||||
.padding(20),
|
||||
|
||||
let header = container(
|
||||
row![
|
||||
colored_icon_view(utility_icon(utility), Some(ENTRY_TEXT)),
|
||||
text(utility_title(utility)).size(20).width(Length::Fill),
|
||||
utility_icon_control(
|
||||
Icon::Close,
|
||||
"Close panel (Esc)",
|
||||
Message::DismissUtility,
|
||||
!busy,
|
||||
),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8),
|
||||
)
|
||||
.padding([6, 10])
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
.style(entry_value_style);
|
||||
let body = scrollable(
|
||||
container(
|
||||
container(content)
|
||||
.padding(16)
|
||||
.width(Length::Fill)
|
||||
.max_width(760)
|
||||
.style(|theme| entry_field_style(theme, false)),
|
||||
)
|
||||
.padding([12, 16])
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill),
|
||||
)
|
||||
.height(Length::Fill);
|
||||
let mut layout = column![header, body].spacing(1);
|
||||
if !matches!(utility, UtilityView::About | UtilityView::Help) {
|
||||
layout = layout.push(
|
||||
container(
|
||||
row![container(text("")).width(Length::Fill), actions]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(2),
|
||||
)
|
||||
.padding([6, 10])
|
||||
.width(Length::Fill)
|
||||
.style(entry_value_style),
|
||||
);
|
||||
}
|
||||
container(layout)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(entry_area_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn search_option<'a>(
|
||||
@@ -5036,7 +5148,7 @@ fn search_option<'a>(
|
||||
message: Message,
|
||||
running: bool,
|
||||
) -> iced::widget::Button<'a, Message> {
|
||||
let option = button(text(format!(
|
||||
let option = utility_button(text(format!(
|
||||
"[{}] {label}",
|
||||
if selected { "x" } else { " " }
|
||||
)));
|
||||
@@ -6103,6 +6215,37 @@ mod tests {
|
||||
let _view = app.view();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utility_panels_share_the_compact_entry_palette() {
|
||||
let theme = Theme::Dark;
|
||||
assert_eq!(
|
||||
entry_area_style(&theme).background,
|
||||
Some(Background::Color(ENTRY_AREA_BACKGROUND))
|
||||
);
|
||||
assert_eq!(
|
||||
entry_field_style(&theme, false).background,
|
||||
Some(Background::Color(ENTRY_FIELD_BACKGROUND))
|
||||
);
|
||||
assert_eq!(
|
||||
entry_input_style(&theme, text_input::Status::Active).background,
|
||||
Background::Color(ENTRY_INPUT_BACKGROUND)
|
||||
);
|
||||
assert_eq!(
|
||||
entry_icon_button(&theme, button::Status::Hovered).background,
|
||||
Some(Background::Color(ENTRY_FIELD_ACTIVE_BACKGROUND))
|
||||
);
|
||||
|
||||
let (_temporary, storage) = fixture_storage();
|
||||
let mut app = test_app(None);
|
||||
app.utility = Some(UtilityView::Settings(SettingsForm::new(&storage)));
|
||||
app.storage = Some(storage);
|
||||
assert_eq!(
|
||||
utility_title(app.utility.as_ref().expect("settings panel")),
|
||||
"Settings"
|
||||
);
|
||||
let _view = app.view();
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct ManualClock(Arc<Mutex<Duration>>);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user