Align desktop utility panels
This commit is contained in:
@@ -3715,6 +3715,7 @@ enum Icon {
|
|||||||
Check,
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Close,
|
||||||
Command,
|
Command,
|
||||||
Copy,
|
Copy,
|
||||||
Delete,
|
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(10.5, 8.0));
|
||||||
path.line_to(Point::new(5.5, 13.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 => {
|
Icon::Command => {
|
||||||
path.move_to(Point::new(2.0, 4.0));
|
path.move_to(Point::new(2.0, 4.0));
|
||||||
path.line_to(Point::new(6.0, 8.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()
|
.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 {
|
fn entry_icon_button(_theme: &Theme, status: button::Status) -> button::Style {
|
||||||
button::Style {
|
button::Style {
|
||||||
background: matches!(status, button::Status::Hovered | button::Status::Pressed)
|
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()
|
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> {
|
fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Message> {
|
||||||
let mut content = column![].spacing(10);
|
let mut content = column![].spacing(10);
|
||||||
match utility {
|
match utility {
|
||||||
UtilityView::About => {
|
UtilityView::About => {
|
||||||
content = content
|
content = content
|
||||||
.push(text(ironstorage::PRODUCT_NAME).size(28))
|
|
||||||
.push(text(format!("Version {}", env!("CARGO_PKG_VERSION"))))
|
.push(text(format!("Version {}", env!("CARGO_PKG_VERSION"))))
|
||||||
.push(text("A native, pass-compatible password-store client."))
|
.push(text("A native, pass-compatible password-store client."))
|
||||||
.push(text("Compatible with pass and pass-otp."))
|
.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) => {
|
UtilityView::Settings(form) => {
|
||||||
content = content
|
let general = column![
|
||||||
.push(text("Settings").size(28))
|
text("General").size(18),
|
||||||
.push(text("Password-store folder"))
|
text("Password-store folder").size(13),
|
||||||
.push(
|
row![
|
||||||
row![
|
text_input("Vault path", &form.vault)
|
||||||
text_input("Vault path", &form.vault)
|
.id(settings_vault_id())
|
||||||
.id(settings_vault_id())
|
.on_input(Message::SettingsVaultChanged)
|
||||||
.on_input(Message::SettingsVaultChanged)
|
.on_submit(Message::SaveSettings)
|
||||||
.on_submit(Message::SaveSettings),
|
.style(entry_input_style),
|
||||||
if form.saving {
|
utility_icon_control(
|
||||||
button("Choose…")
|
Icon::Folder,
|
||||||
} else {
|
"Choose password-store folder",
|
||||||
button("Choose…").on_press(Message::PickSettingsVault)
|
Message::PickSettingsVault,
|
||||||
},
|
!form.saving,
|
||||||
]
|
),
|
||||||
.spacing(8),
|
]
|
||||||
)
|
.align_y(iced::Alignment::Center)
|
||||||
.push(text("Default OpenPGP key fingerprint or identity"))
|
.spacing(6),
|
||||||
.push(
|
text("Default OpenPGP key fingerprint or identity").size(13),
|
||||||
text_input("Default key", &form.default_key)
|
text_input("Default key", &form.default_key)
|
||||||
.on_input(Message::SettingsDefaultKeyChanged)
|
.on_input(Message::SettingsDefaultKeyChanged)
|
||||||
.on_submit(Message::SaveSettings),
|
.on_submit(Message::SaveSettings)
|
||||||
)
|
.style(entry_input_style),
|
||||||
.push(text(
|
text("Authentication inactivity timeout in seconds (1–86400)").size(13),
|
||||||
"Authentication inactivity timeout in seconds (1–86400)",
|
text_input("Timeout seconds", &form.authentication_timeout)
|
||||||
))
|
.on_input(Message::SettingsTimeoutChanged)
|
||||||
.push(
|
.on_submit(Message::SaveSettings)
|
||||||
text_input("Timeout seconds", &form.authentication_timeout)
|
.style(entry_input_style),
|
||||||
.on_input(Message::SettingsTimeoutChanged)
|
]
|
||||||
.on_submit(Message::SaveSettings),
|
.spacing(8);
|
||||||
)
|
content = content.push(
|
||||||
.push(text("Command-line tools"))
|
container(general)
|
||||||
.push(text(
|
.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.",
|
"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 {
|
.size(13),
|
||||||
button("Install CLI and TUI Links")
|
]
|
||||||
} else {
|
.spacing(8);
|
||||||
button("Install CLI and TUI Links").on_press(Message::InstallCommandLinks)
|
|
||||||
});
|
|
||||||
if let Some(status) = &form.command_links_status {
|
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 {
|
if let Some(storage) = &app.storage {
|
||||||
content = content
|
content = content.push(
|
||||||
.push(text(format!(
|
container(
|
||||||
|
column![
|
||||||
|
text("Configuration details").size(18),
|
||||||
|
text(format!(
|
||||||
"Shared configuration: {}",
|
"Shared configuration: {}",
|
||||||
storage.config_source().display()
|
storage.config_source().display()
|
||||||
)))
|
))
|
||||||
.push(text(format!(
|
.size(13),
|
||||||
"Clipboard cleanup remains {} seconds.",
|
text(format!(
|
||||||
storage.clipboard_timeout().duration().as_secs()
|
"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(
|
||||||
|
"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 {
|
if let Some(error) = &form.error {
|
||||||
content = content.push(text(format!("Settings error: {error}")));
|
content = content.push(text(format!("Settings error: {error}")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UtilityView::Recipients(form) => {
|
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 {
|
match form.kind {
|
||||||
RecipientWorkflowKind::InitializeStore => {
|
RecipientWorkflowKind::InitializeStore => {
|
||||||
content = content.push(text(
|
content = content.push(text(
|
||||||
@@ -4242,7 +4350,8 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
|||||||
.push(
|
.push(
|
||||||
text_input("team/services", &form.path)
|
text_input("team/services", &form.path)
|
||||||
.on_input(Message::RecipientPathChanged)
|
.on_input(Message::RecipientPathChanged)
|
||||||
.on_submit(Message::SubmitRecipient),
|
.on_submit(Message::SubmitRecipient)
|
||||||
|
.style(entry_input_style),
|
||||||
)
|
)
|
||||||
.push(text(
|
.push(text(
|
||||||
"An upstream-compatible nested .gpg-id persists the folder and controls its recipients.",
|
"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 {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
let choice = button(text(format!(
|
let choice = utility_button(text(format!(
|
||||||
"[{}] {}{}",
|
"[{}] {}{}",
|
||||||
if recipient.selected { "x" } else { " " },
|
if recipient.selected { "x" } else { " " },
|
||||||
recipient.label,
|
recipient.label,
|
||||||
@@ -4269,7 +4378,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
|||||||
};
|
};
|
||||||
if form.kind == RecipientWorkflowKind::InitializeStore {
|
if form.kind == RecipientWorkflowKind::InitializeStore {
|
||||||
let selected_default = recipient.fingerprint == form.default_key;
|
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"
|
"Public recipient only"
|
||||||
} else if selected_default {
|
} else if selected_default {
|
||||||
"Default key ✓"
|
"Default key ✓"
|
||||||
@@ -4286,7 +4395,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
|||||||
content = content.push(choice);
|
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",
|
"[{}] Replace this recipient policy and selectively re-encrypt affected entries",
|
||||||
if form.confirmed { "x" } else { " " }
|
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) => {
|
UtilityView::NewEntry(form) => {
|
||||||
content = content
|
content = content.push(text("Entry path")).push(
|
||||||
.push(text("New Entry").size(28))
|
text_input("folder/account", &form.path)
|
||||||
.push(text("Entry path"))
|
.on_input(Message::NewEntryPathChanged)
|
||||||
.push(
|
.on_submit(Message::SubmitNewEntry)
|
||||||
text_input("folder/account", &form.path)
|
.style(entry_input_style),
|
||||||
.on_input(Message::NewEntryPathChanged)
|
);
|
||||||
.on_submit(Message::SubmitNewEntry),
|
let generate = utility_button(text(format!(
|
||||||
);
|
|
||||||
let generate = button(text(format!(
|
|
||||||
"[{}] Generate the initial password with storage policy",
|
"[{}] Generate the initial password with storage policy",
|
||||||
if form.generate { "x" } else { " " }
|
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(
|
content = content.push(text("Password length")).push(
|
||||||
text_input("25", &form.length)
|
text_input("25", &form.length)
|
||||||
.on_input(Message::NewEntryLengthChanged)
|
.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",
|
"[{}] Letters and digits only",
|
||||||
if form.no_symbols { "x" } else { " " }
|
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) => {
|
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(
|
let input = text_input(
|
||||||
if form.mode == SearchMode::Names {
|
if form.mode == SearchMode::Names {
|
||||||
"Name substring"
|
"Name substring"
|
||||||
@@ -4355,9 +4458,10 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
|||||||
&form.query,
|
&form.query,
|
||||||
)
|
)
|
||||||
.on_input(Message::SearchQueryChanged)
|
.on_input(Message::SearchQueryChanged)
|
||||||
.on_submit(Message::SubmitSearch);
|
.on_submit(Message::SubmitSearch)
|
||||||
|
.style(entry_input_style);
|
||||||
content = if form.mode == SearchMode::Names {
|
content = if form.mode == SearchMode::Names {
|
||||||
let add = button("Add another term");
|
let add = utility_button("Add another term");
|
||||||
content.push(
|
content.push(
|
||||||
row![
|
row![
|
||||||
input,
|
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.",
|
"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() {
|
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 {
|
content = content.push(if form.running {
|
||||||
remove
|
remove
|
||||||
} else {
|
} else {
|
||||||
@@ -4416,7 +4520,7 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
|||||||
"Entry"
|
"Entry"
|
||||||
};
|
};
|
||||||
content = content.push(
|
content = content.push(
|
||||||
button(text(format!("{kind} · {}", matched.path())))
|
utility_button(text(format!("{kind} · {}", matched.path())))
|
||||||
.on_press(Message::ActivateSearchResult(matched.id().clone())),
|
.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() {
|
for entry in results.entries() {
|
||||||
content = content.push(
|
content = content.push(
|
||||||
button(text(format!("Entry · {}", entry.path()))).on_press(
|
utility_button(text(format!("Entry · {}", entry.path()))).on_press(
|
||||||
Message::ActivateSearchResult(TreeNodeId::Entry(
|
Message::ActivateSearchResult(TreeNodeId::Entry(
|
||||||
entry.path().clone(),
|
entry.path().clone(),
|
||||||
)),
|
)),
|
||||||
@@ -4450,35 +4554,29 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
UtilityView::Mutation(form) => {
|
UtilityView::Mutation(form) => {
|
||||||
let title = match form.kind {
|
content = content.push(text(format!("Source: {}", form.source.path().display())));
|
||||||
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())));
|
|
||||||
if form.kind != MutationKind::Delete {
|
if form.kind != MutationKind::Delete {
|
||||||
content = content
|
content = content
|
||||||
.push(text("Destination entry path or existing folder"))
|
.push(text("Destination entry path or existing folder"))
|
||||||
.push(
|
.push(
|
||||||
text_input("folder/name", &form.destination)
|
text_input("folder/name", &form.destination)
|
||||||
.on_input(Message::MutationDestinationChanged)
|
.on_input(Message::MutationDestinationChanged)
|
||||||
.on_submit(Message::SubmitMutation),
|
.on_submit(Message::SubmitMutation)
|
||||||
|
.style(entry_input_style),
|
||||||
)
|
)
|
||||||
.push(text("Choose an existing destination folder"));
|
.push(text("Choose an existing destination folder"));
|
||||||
let mut destinations = row![].spacing(6);
|
let mut destinations = row![].spacing(6);
|
||||||
for directory in app.navigation.directories() {
|
for directory in app.navigation.directories() {
|
||||||
let path = directory.path().display().to_string();
|
let path = directory.path().display().to_string();
|
||||||
let label = if path.is_empty() { "Store root" } else { &path };
|
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 {
|
destinations = destinations.push(if form.running {
|
||||||
choice
|
choice
|
||||||
} else {
|
} else {
|
||||||
choice.on_press(Message::SelectMutationDestination(path))
|
choice.on_press(Message::SelectMutationDestination(path))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let overwrite = button(text(format!(
|
let overwrite = utility_button(text(format!(
|
||||||
"[{}] Replace an existing destination entry",
|
"[{}] Replace an existing destination entry",
|
||||||
if form.overwrite { "x" } else { " " }
|
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)
|
overwrite.on_press(Message::ToggleMutationOverwrite)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let confirmation = button(text(format!(
|
let confirmation = utility_button(text(format!(
|
||||||
"[{}] Permanently remove this {} and commit the deletion",
|
"[{}] Permanently remove this {} and commit the deletion",
|
||||||
if form.confirmed { "x" } else { " " },
|
if form.confirmed { "x" } else { " " },
|
||||||
if form.source.is_directory() {
|
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) => {
|
UtilityView::Git(form) => {
|
||||||
content = content
|
content = content.push(text(
|
||||||
.push(text("Git Synchronization").size(28))
|
"All repository, HTTPS transport, credential, merge, and conflict decisions are owned by crates/storage. No git process or credential helper is launched.",
|
||||||
.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 {
|
if let Some(phase) = form.progress {
|
||||||
content = content.push(text(format!("Progress: {}", git_phase_name(phase))));
|
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.",
|
"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() {
|
for (index, selection) in form.conflicts.iter().enumerate() {
|
||||||
let local = button(text(
|
let local = utility_button(text(
|
||||||
if selection.choice == Some(GitConflictChoice::Local) {
|
if selection.choice == Some(GitConflictChoice::Local) {
|
||||||
"Local ✓"
|
"Local ✓"
|
||||||
} else {
|
} else {
|
||||||
"Use local"
|
"Use local"
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
let remote = button(text(
|
let remote = utility_button(text(
|
||||||
if selection.choice == Some(GitConflictChoice::Remote) {
|
if selection.choice == Some(GitConflictChoice::Remote) {
|
||||||
"Remote ✓"
|
"Remote ✓"
|
||||||
} else {
|
} else {
|
||||||
@@ -4619,14 +4715,14 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
|||||||
}
|
}
|
||||||
UtilityView::Otp(form) => {
|
UtilityView::Otp(form) => {
|
||||||
content = content
|
content = content
|
||||||
.push(text("One-Time Password").size(28))
|
|
||||||
.push(text(
|
.push(text(
|
||||||
"Code generation, counters, URI validation, QR payloads, entry mutation, Git commits, and clipboard policy are owned by crates/storage.",
|
"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("Entry path"))
|
||||||
.push(
|
.push(
|
||||||
text_input("folder/entry", &form.entry)
|
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 {
|
if let Some(display) = &app.sensitive.otp {
|
||||||
content = content
|
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("Import or replace OTP").size(20))
|
||||||
.push(
|
.push(
|
||||||
text_input("otpauth://…", &form.uri)
|
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(
|
.push(search_option(
|
||||||
"Replace an existing OTP URI",
|
"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) => {
|
UtilityView::Kdbx(form) => {
|
||||||
content = content
|
content = content
|
||||||
.push(text("Import KeePass Database").size(28))
|
|
||||||
.push(text(
|
.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.",
|
"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![
|
row![
|
||||||
text_input("Database.kdbx", &form.source)
|
text_input("Database.kdbx", &form.source)
|
||||||
.on_input(Message::KdbxSourceChanged)
|
.on_input(Message::KdbxSourceChanged)
|
||||||
.on_submit(Message::SubmitKdbxImport),
|
.on_submit(Message::SubmitKdbxImport)
|
||||||
|
.style(entry_input_style),
|
||||||
if form.running {
|
if form.running {
|
||||||
button("Choose…")
|
utility_button("Choose…")
|
||||||
} else {
|
} else {
|
||||||
button("Choose…").on_press(Message::PickKdbxSource)
|
utility_button("Choose…").on_press(Message::PickKdbxSource)
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
.spacing(8),
|
.spacing(8),
|
||||||
@@ -4724,11 +4821,12 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
|||||||
row![
|
row![
|
||||||
text_input("No key file", &form.key_file)
|
text_input("No key file", &form.key_file)
|
||||||
.on_input(Message::KdbxKeyFileChanged)
|
.on_input(Message::KdbxKeyFileChanged)
|
||||||
.on_submit(Message::SubmitKdbxImport),
|
.on_submit(Message::SubmitKdbxImport)
|
||||||
|
.style(entry_input_style),
|
||||||
if form.running {
|
if form.running {
|
||||||
button("Choose…")
|
utility_button("Choose…")
|
||||||
} else {
|
} else {
|
||||||
button("Choose…").on_press(Message::PickKdbxKeyFile)
|
utility_button("Choose…").on_press(Message::PickKdbxKeyFile)
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
.spacing(8),
|
.spacing(8),
|
||||||
@@ -4738,7 +4836,8 @@ fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Messa
|
|||||||
text_input("Password", &form.password)
|
text_input("Password", &form.password)
|
||||||
.secure(true)
|
.secure(true)
|
||||||
.on_input(|value| Message::KdbxPasswordChanged(Zeroizing::new(value)))
|
.on_input(|value| Message::KdbxPasswordChanged(Zeroizing::new(value)))
|
||||||
.on_submit(Message::SubmitKdbxImport),
|
.on_submit(Message::SubmitKdbxImport)
|
||||||
|
.style(entry_input_style),
|
||||||
)
|
)
|
||||||
.push(search_option(
|
.push(search_option(
|
||||||
"Quick add: only add entries not already present",
|
"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 => {
|
UtilityView::Help => {
|
||||||
content = content
|
content = content.push(text("Keyboard shortcuts").size(22));
|
||||||
.push(text("IronStorage Help").size(28))
|
|
||||||
.push(text("Keyboard shortcuts").size(22));
|
|
||||||
for spec in action::ACTIONS {
|
for spec in action::ACTIONS {
|
||||||
if let Some(shortcut) = action::shortcut_label(spec.action) {
|
if let Some(shortcut) = action::shortcut_label(spec.action) {
|
||||||
content = content.push(text(format!(
|
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)
|
let busy = matches!(utility, UtilityView::Settings(form) if form.saving)
|
||||||
|| matches!(utility, UtilityView::Recipients(form) if form.running)
|
|| matches!(utility, UtilityView::Recipients(form) if form.running)
|
||||||
|| matches!(utility, UtilityView::NewEntry(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::Git(form) if form.running)
|
||||||
|| matches!(utility, UtilityView::Otp(form) if form.running)
|
|| matches!(utility, UtilityView::Otp(form) if form.running)
|
||||||
|| matches!(utility, UtilityView::Kdbx(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 {
|
let actions = match utility {
|
||||||
UtilityView::Settings(form) => {
|
UtilityView::Settings(form) => row![utility_icon_control(
|
||||||
let save = button(if form.saving {
|
Icon::Check,
|
||||||
"Validating…"
|
if form.saving {
|
||||||
|
"Validating settings…"
|
||||||
} else {
|
} else {
|
||||||
"Save Settings"
|
"Save settings"
|
||||||
});
|
},
|
||||||
row![
|
Message::SaveSettings,
|
||||||
if form.saving {
|
!form.saving,
|
||||||
save
|
)],
|
||||||
} else {
|
UtilityView::Recipients(form) => row![utility_icon_control(
|
||||||
save.on_press(Message::SaveSettings)
|
Icon::Check,
|
||||||
},
|
if form.running {
|
||||||
done,
|
"Applying recipient policy…"
|
||||||
]
|
|
||||||
.spacing(8)
|
|
||||||
}
|
|
||||||
UtilityView::Recipients(form) => {
|
|
||||||
let apply = button(if form.running {
|
|
||||||
"Applying…"
|
|
||||||
} else {
|
} else {
|
||||||
"Apply Recipient Policy"
|
"Apply recipient policy"
|
||||||
});
|
},
|
||||||
row![
|
Message::SubmitRecipient,
|
||||||
if form.running {
|
!form.running,
|
||||||
apply
|
)],
|
||||||
} else {
|
UtilityView::NewEntry(form) => row![utility_icon_control(
|
||||||
apply.on_press(Message::SubmitRecipient)
|
Icon::Check,
|
||||||
},
|
if form.running {
|
||||||
done,
|
"Preparing draft…"
|
||||||
]
|
|
||||||
.spacing(8)
|
|
||||||
}
|
|
||||||
UtilityView::NewEntry(form) => {
|
|
||||||
let create = button(if form.running {
|
|
||||||
"Preparing…"
|
|
||||||
} else {
|
} else {
|
||||||
"Create Draft"
|
"Create draft"
|
||||||
});
|
},
|
||||||
row![
|
Message::SubmitNewEntry,
|
||||||
if form.running {
|
!form.running,
|
||||||
create
|
)],
|
||||||
} else {
|
UtilityView::Search(form) => row![utility_icon_control(
|
||||||
create.on_press(Message::SubmitNewEntry)
|
Icon::Search,
|
||||||
},
|
if form.running {
|
||||||
done,
|
|
||||||
]
|
|
||||||
.spacing(8)
|
|
||||||
}
|
|
||||||
UtilityView::Search(form) => {
|
|
||||||
let submit = button(if form.running {
|
|
||||||
"Searching…"
|
"Searching…"
|
||||||
} else {
|
} else {
|
||||||
"Search"
|
"Search"
|
||||||
});
|
},
|
||||||
row![
|
Message::SubmitSearch,
|
||||||
if form.running {
|
!form.running,
|
||||||
submit
|
)],
|
||||||
} else {
|
|
||||||
submit.on_press(Message::SubmitSearch)
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
]
|
|
||||||
.spacing(8)
|
|
||||||
}
|
|
||||||
UtilityView::Mutation(form) => {
|
UtilityView::Mutation(form) => {
|
||||||
let submit = button(if form.running {
|
let icon = match form.kind {
|
||||||
"Applying…"
|
MutationKind::Move => Icon::Move,
|
||||||
} else {
|
MutationKind::Copy => Icon::Copy,
|
||||||
match form.kind {
|
MutationKind::Delete => Icon::Delete,
|
||||||
MutationKind::Move => "Move",
|
};
|
||||||
MutationKind::Copy => "Copy",
|
row![utility_icon_control(
|
||||||
MutationKind::Delete => "Delete",
|
icon,
|
||||||
}
|
|
||||||
});
|
|
||||||
row![
|
|
||||||
if form.running {
|
if form.running {
|
||||||
submit
|
"Applying mutation…"
|
||||||
} else {
|
} else {
|
||||||
submit.on_press(Message::SubmitMutation)
|
"Apply mutation"
|
||||||
},
|
},
|
||||||
done,
|
Message::SubmitMutation,
|
||||||
]
|
!form.running,
|
||||||
.spacing(8)
|
)]
|
||||||
}
|
}
|
||||||
UtilityView::Git(form) => {
|
UtilityView::Git(form) => {
|
||||||
if form.running {
|
if form.running {
|
||||||
row![
|
row![utility_button("Cancel Git operation").on_press(Message::CancelGit)]
|
||||||
button("Cancel Git operation").on_press(Message::CancelGit),
|
|
||||||
done
|
|
||||||
]
|
|
||||||
.spacing(8)
|
|
||||||
} else {
|
} else {
|
||||||
let mut actions = row![
|
let mut actions = row![
|
||||||
icon_control(
|
utility_icon_control(
|
||||||
Icon::Refresh,
|
Icon::Refresh,
|
||||||
"Refresh Git status".to_owned(),
|
"Refresh Git status",
|
||||||
Message::RunGit(DesktopGitRequest::Refresh)
|
Message::RunGit(DesktopGitRequest::Refresh),
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
icon_control(
|
utility_icon_control(
|
||||||
Icon::Down,
|
Icon::Down,
|
||||||
"Pull from remote".to_owned(),
|
"Pull from remote",
|
||||||
Message::RunGit(DesktopGitRequest::Pull)
|
Message::RunGit(DesktopGitRequest::Pull),
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
icon_control(
|
utility_icon_control(
|
||||||
Icon::Up,
|
Icon::Up,
|
||||||
"Push to remote".to_owned(),
|
"Push to remote",
|
||||||
Message::RunGit(DesktopGitRequest::Push)
|
Message::RunGit(DesktopGitRequest::Push),
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
icon_control(
|
utility_icon_control(
|
||||||
Icon::Refresh,
|
Icon::Refresh,
|
||||||
"Synchronize with remote".to_owned(),
|
"Synchronize with remote",
|
||||||
Message::RunGit(DesktopGitRequest::Sync)
|
Message::RunGit(DesktopGitRequest::Sync),
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.spacing(2);
|
.spacing(2);
|
||||||
if !form.conflicts.is_empty() {
|
if !form.conflicts.is_empty() {
|
||||||
actions = actions.push(icon_control(
|
actions = actions.push(utility_icon_control(
|
||||||
Icon::Check,
|
Icon::Check,
|
||||||
"Resolve selected versions".to_owned(),
|
"Resolve selected versions",
|
||||||
Message::ResolveGitConflicts,
|
Message::ResolveGitConflicts,
|
||||||
|
true,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
actions.push(done)
|
actions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UtilityView::Otp(form) => {
|
UtilityView::Otp(form) => {
|
||||||
if form.running {
|
if form.running {
|
||||||
row![button("Working…"), done].spacing(8)
|
row![utility_icon_control(
|
||||||
|
Icon::Refresh,
|
||||||
|
"Working…",
|
||||||
|
Message::RunOtpCode(false),
|
||||||
|
false,
|
||||||
|
)]
|
||||||
} else {
|
} else {
|
||||||
let mut actions = row![
|
let mut actions = row![
|
||||||
icon_control(
|
utility_icon_control(
|
||||||
Icon::Refresh,
|
Icon::Refresh,
|
||||||
"Generate OTP code".to_owned(),
|
"Generate OTP code",
|
||||||
Message::RunOtpCode(false)
|
Message::RunOtpCode(false),
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
icon_control(
|
utility_icon_control(
|
||||||
Icon::Copy,
|
Icon::Copy,
|
||||||
"Copy OTP code".to_owned(),
|
"Copy OTP code",
|
||||||
Message::RunOtpCode(true)
|
Message::RunOtpCode(true),
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
icon_control(
|
utility_icon_control(
|
||||||
Icon::Link,
|
Icon::Link,
|
||||||
"Show provisioning URI".to_owned(),
|
"Show provisioning URI",
|
||||||
Message::RunOtpUri {
|
Message::RunOtpUri {
|
||||||
qr: false,
|
qr: false,
|
||||||
copy: false,
|
copy: false,
|
||||||
}
|
},
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
icon_control(
|
utility_icon_control(
|
||||||
Icon::Copy,
|
Icon::Copy,
|
||||||
"Copy provisioning URI".to_owned(),
|
"Copy provisioning URI",
|
||||||
Message::RunOtpUri {
|
Message::RunOtpUri {
|
||||||
qr: false,
|
qr: false,
|
||||||
copy: true,
|
copy: true,
|
||||||
}
|
},
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
icon_control(
|
utility_icon_control(
|
||||||
Icon::Qr,
|
Icon::Qr,
|
||||||
"Show provisioning QR code".to_owned(),
|
"Show provisioning QR code",
|
||||||
Message::RunOtpUri {
|
Message::RunOtpUri {
|
||||||
qr: true,
|
qr: true,
|
||||||
copy: false,
|
copy: false,
|
||||||
}
|
},
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.spacing(2);
|
.spacing(2);
|
||||||
if form.hotp_confirmation {
|
if form.hotp_confirmation {
|
||||||
actions = actions.push(
|
actions = actions.push(
|
||||||
button("Confirm HOTP counter advance").on_press(Message::ConfirmHotp),
|
utility_button("Confirm HOTP counter advance")
|
||||||
|
.on_press(Message::ConfirmHotp),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
actions
|
actions
|
||||||
.push(icon_control(
|
.push(utility_icon_control(
|
||||||
Icon::Down,
|
Icon::Down,
|
||||||
"Import provisioning URI".to_owned(),
|
"Import provisioning URI",
|
||||||
Message::SubmitOtpImport,
|
Message::SubmitOtpImport,
|
||||||
|
true,
|
||||||
))
|
))
|
||||||
.push(icon_control(
|
.push(utility_icon_control(
|
||||||
Icon::Qr,
|
Icon::Qr,
|
||||||
"Import provisioning QR image".to_owned(),
|
"Import provisioning QR image",
|
||||||
Message::PickOtpQr,
|
Message::PickOtpQr,
|
||||||
|
true,
|
||||||
))
|
))
|
||||||
.push(
|
.push(
|
||||||
button("Remove OTP")
|
utility_button("Remove OTP")
|
||||||
.style(button::danger)
|
.style(button::danger)
|
||||||
.on_press(Message::SubmitOtpRemoval),
|
.on_press(Message::SubmitOtpRemoval),
|
||||||
)
|
)
|
||||||
.push(done)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UtilityView::Kdbx(form) => {
|
UtilityView::Kdbx(form) => row![utility_icon_control(
|
||||||
let import = button(if form.running {
|
Icon::Down,
|
||||||
|
if form.running {
|
||||||
"Importing…"
|
"Importing…"
|
||||||
} else if form.quick_add {
|
} else if form.quick_add {
|
||||||
"Quick Add"
|
"Quick add KeePass entries"
|
||||||
} else {
|
} else {
|
||||||
"Import"
|
"Import KeePass entries"
|
||||||
});
|
},
|
||||||
row![
|
Message::SubmitKdbxImport,
|
||||||
if form.running {
|
!form.running,
|
||||||
import
|
)],
|
||||||
} else {
|
UtilityView::About | UtilityView::Help => row![container(text("")).width(Length::Fill)],
|
||||||
import.on_press(Message::SubmitKdbxImport)
|
|
||||||
},
|
|
||||||
done,
|
|
||||||
]
|
|
||||||
.spacing(8)
|
|
||||||
}
|
|
||||||
UtilityView::About | UtilityView::Help => row![done],
|
|
||||||
};
|
};
|
||||||
container(
|
|
||||||
column![scrollable(content).height(Length::Fill), actions,]
|
let header = container(
|
||||||
.spacing(12)
|
row![
|
||||||
.padding(20),
|
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)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.style(entry_value_style);
|
||||||
.into()
|
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>(
|
fn search_option<'a>(
|
||||||
@@ -5036,7 +5148,7 @@ fn search_option<'a>(
|
|||||||
message: Message,
|
message: Message,
|
||||||
running: bool,
|
running: bool,
|
||||||
) -> iced::widget::Button<'a, Message> {
|
) -> iced::widget::Button<'a, Message> {
|
||||||
let option = button(text(format!(
|
let option = utility_button(text(format!(
|
||||||
"[{}] {label}",
|
"[{}] {label}",
|
||||||
if selected { "x" } else { " " }
|
if selected { "x" } else { " " }
|
||||||
)));
|
)));
|
||||||
@@ -6103,6 +6215,37 @@ mod tests {
|
|||||||
let _view = app.view();
|
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)]
|
#[derive(Clone, Default)]
|
||||||
struct ManualClock(Arc<Mutex<Duration>>);
|
struct ManualClock(Arc<Mutex<Duration>>);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ requires every registered action ID to remain present in this document.
|
|||||||
| Area and compatible operation | Registered desktop action and menu | Direct control, dialog, or view | Command palette |
|
| Area and compatible operation | Registered desktop action and menu | Direct control, dialog, or view | Command palette |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Configuration and lock: open configured store | `open-folder` (File) | Compact toolbar control and native folder picker; storage validates and persists configuration | Yes |
|
| Configuration and lock: open configured store | `open-folder` (File) | Compact toolbar control and native folder picker; storage validates and persists configuration | Yes |
|
||||||
| Configuration and lock: edit shared settings | `settings` (IronStorage) | Labelled Settings form for vault, default key, and inactivity timeout | Yes |
|
| Configuration and lock: edit shared settings | `settings` (IronStorage) | Compact shared utility shell with grouped vault, default-key, timeout, command-link, and configuration-detail panels | Yes |
|
||||||
| Configuration and lock: refresh typed tree | `refresh` (View) | Sidebar Refresh control | Yes |
|
| Configuration and lock: refresh typed tree | `refresh` (View) | Sidebar Refresh control | Yes |
|
||||||
| Configuration and lock: lock/unlock | `lock` (Tools) | Toolbar Lock control; opening protected content starts storage authentication | Lock only; unlock is the protected action being resumed |
|
| Configuration and lock: lock/unlock | `lock` (Tools) | Toolbar Lock control; opening protected content starts storage authentication | Lock only; unlock is the protected action being resumed |
|
||||||
| Base pass: `init` root or nested recipient policy | `initialize-store`, `new-folder` (File) | Recipient/default-key form and explicit replacement confirmation | Yes |
|
| Base pass: `init` root or nested recipient policy | `initialize-store`, `new-folder` (File) | Recipient/default-key form and explicit replacement confirmation | Yes |
|
||||||
@@ -58,9 +58,11 @@ These are deliberate presentation differences, not storage-feature gaps.
|
|||||||
rows wrap, long names and storage-grouped multiline values are retained, and
|
rows wrap, long names and storage-grouped multiline values are retained, and
|
||||||
the supported narrow window floor is 480 by 360 logical pixels. Iced/winit applies native
|
the supported narrow window floor is 480 by 360 logical pixels. Iced/winit applies native
|
||||||
display scaling before layout.
|
display scaling before layout.
|
||||||
- Compact toolbar controls use one 16-by-16 vector icon system with descriptive
|
- Compact main and utility toolbar controls use one 16-by-16 vector icon system
|
||||||
delayed tooltips and registered shortcuts. Focused/selected rows use the
|
with descriptive delayed tooltips and registered shortcuts. Every utility
|
||||||
theme's primary contrast pair; field labels remain visible as a non-colour cue.
|
panel shares the entry palette, flat controls, responsive 760-pixel content
|
||||||
|
cap, and compact header/footer chrome; field labels remain visible as a
|
||||||
|
non-colour cue.
|
||||||
- The app implements no animation or motion-driven state transition. The one-
|
- The app implements no animation or motion-driven state transition. The one-
|
||||||
second subscription updates lease, OTP, Git, and clipboard presentation state
|
second subscription updates lease, OTP, Git, and clipboard presentation state
|
||||||
without moving focus or renewing authentication, so reduced-motion mode has
|
without moving focus or renewing authentication, so reduced-motion mode has
|
||||||
|
|||||||
Reference in New Issue
Block a user