Show OTP codes above desktop entry fields
This commit is contained in:
@@ -26,8 +26,8 @@ use iced::{
|
||||
Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Size,
|
||||
Subscription, Task, Theme, event, keyboard, mouse, time, touch,
|
||||
widget::{
|
||||
button, canvas, column, container, mouse_area, pane_grid, row, scrollable, text,
|
||||
text_editor, text_input, tooltip,
|
||||
button, canvas, column, container, mouse_area, pane_grid, progress_bar, row, scrollable,
|
||||
text, text_editor, text_input, tooltip,
|
||||
},
|
||||
window,
|
||||
};
|
||||
@@ -5341,11 +5341,70 @@ fn otp_code_text(display: &OtpDisplay) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn matching_otp_display<'a>(app: &'a App, entry: &str) -> Option<&'a OtpDisplay> {
|
||||
app.sensitive
|
||||
.otp
|
||||
.as_ref()
|
||||
.filter(|display| display.entry == entry)
|
||||
}
|
||||
|
||||
fn otp_progress(display: &OtpDisplay) -> Option<(u64, u64)> {
|
||||
display.validity.period().map(|period| {
|
||||
(
|
||||
display
|
||||
.remaining_at(display.observed_at)
|
||||
.unwrap_or_default()
|
||||
.min(period),
|
||||
period.max(1),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn viewer_view<'a>(app: &'a App, editor: &'a EntryEditor) -> Element<'a, Message> {
|
||||
let entry = editor.entry();
|
||||
let mut rows =
|
||||
column![text("Up/Down/Home/End select fields · ⌘C copies the selected value").size(12),]
|
||||
.spacing(8);
|
||||
let mut rows = column![].spacing(8);
|
||||
if let Some(display) = matching_otp_display(app, &entry) {
|
||||
let mut code = column![
|
||||
text(match display.metadata.kind() {
|
||||
OtpKind::Totp => "Current one-time password",
|
||||
OtpKind::Hotp => "Generated one-time password",
|
||||
})
|
||||
.size(13),
|
||||
text(otp_code_text(display)).size(42),
|
||||
]
|
||||
.align_x(iced::Alignment::Center)
|
||||
.spacing(2);
|
||||
if let Some((remaining, period)) = otp_progress(display) {
|
||||
code = code.push(text(format!("{remaining}s remaining"))).push(
|
||||
progress_bar(0.0..=period as f32, remaining as f32).girth(Length::Fixed(6.0)),
|
||||
);
|
||||
} else if let Some(counter) = display.validity.counter() {
|
||||
code = code.push(text(format!("HOTP counter {counter}")));
|
||||
}
|
||||
rows = rows.push(
|
||||
container(code)
|
||||
.padding([10, 12])
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
.style(entry_value_style),
|
||||
);
|
||||
} else if app.otp_pending
|
||||
&& editor
|
||||
.document()
|
||||
.fields()
|
||||
.iter()
|
||||
.any(|field| field.metadata().otp().is_some())
|
||||
{
|
||||
rows = rows.push(
|
||||
container(text("Generating one-time password…").size(15))
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
.style(entry_value_style),
|
||||
);
|
||||
}
|
||||
rows =
|
||||
rows.push(text("Up/Down/Home/End select fields · ⌘C copies the selected value").size(12));
|
||||
|
||||
for field in editor.document().display_fields() {
|
||||
let id = field.id();
|
||||
@@ -5392,34 +5451,6 @@ fn viewer_view<'a>(app: &'a App, editor: &'a EntryEditor) -> Element<'a, Message
|
||||
|| format!("counter {}", otp.counter().unwrap_or_default()),
|
||||
|period| format!("{period}s period"),
|
||||
);
|
||||
let code = app
|
||||
.sensitive
|
||||
.otp
|
||||
.as_ref()
|
||||
.filter(|display| display.entry == entry);
|
||||
field_view = field_view.push(match code {
|
||||
Some(display) => column![
|
||||
text(otp_code_text(display)).size(28),
|
||||
text(display.remaining_at(display.observed_at).map_or_else(
|
||||
|| format!(
|
||||
"HOTP counter {}",
|
||||
display.validity.counter().unwrap_or_default()
|
||||
),
|
||||
|remaining| format!("Valid for {remaining}s"),
|
||||
))
|
||||
.size(12),
|
||||
]
|
||||
.spacing(1),
|
||||
None if app.otp_pending => column![text("Generating one-time password…").size(13)],
|
||||
None => column![
|
||||
text(if otp.kind() == OtpKind::Hotp {
|
||||
"Generate the next HOTP code to advance its counter."
|
||||
} else {
|
||||
"One-time password unavailable. Refresh to retry."
|
||||
})
|
||||
.size(13)
|
||||
],
|
||||
});
|
||||
field_view = field_view.push(
|
||||
text(format!(
|
||||
"{:?} · {} · {} · {:?} · {} digits · {cadence}",
|
||||
@@ -6783,6 +6814,8 @@ mod tests {
|
||||
};
|
||||
assert_eq!(display.remaining_at(59), Some(1));
|
||||
assert_eq!(display.remaining_at(60), Some(0));
|
||||
assert_eq!(otp_progress(&display), Some((1, 30)));
|
||||
assert_eq!(otp_code_text(&display), "9428 7082");
|
||||
|
||||
let mut hotp_document = storage
|
||||
.create_document("otp/rfc4226", &mut provider)
|
||||
|
||||
@@ -25,7 +25,7 @@ requires every registered action ID to remain present in this document.
|
||||
| Base pass: `grep` | `search-contents` (Edit) | Authenticated decrypted-search form and typed result activation | Yes |
|
||||
| Base pass: `mv`/`rename`, `cp`/`copy`, `rm`/`remove` | `move-entry`, `copy-entry`, `delete-entry` (Entry) | Sidebar context controls and validated mutation forms; delete is confirmed | Yes |
|
||||
| KeePass migration: additive KDBX import and quick add | `import-kdbx` (Tools) | Native file/key picker, protected password input, explicit confirmation, and storage-owned tree refresh | Yes |
|
||||
| Pass OTP: code/show and timed copy | `generate-otp`, `copy-otp` (Entry) | OTP panel shows typed metadata, code, validity, and HOTP confirmation | Yes |
|
||||
| Pass OTP: code/show and timed copy | `generate-otp`, `copy-otp` (Entry) | Unlocked entry header shows the large code and storage-period countdown bar; the OTP field retains metadata/actions and HOTP confirmation | Yes |
|
||||
| Pass OTP: insert/add/append and validate | `import-otp` (Entry) | URI/QR import form; storage validates, replaces, and commits | Yes |
|
||||
| Pass OTP: URI terminal/clipboard/QR | `show-otp-uri`, `copy-otp-uri`, `show-otp-qr` (Entry) | Explicit secret view, timed copy, and storage-provided QR matrix | Yes |
|
||||
| Pass OTP: remove | `remove-otp` (Entry) | Explicit permanent-removal confirmation | Yes |
|
||||
|
||||
Reference in New Issue
Block a user