feat: entry search
This commit is contained in:
parent
8e6fa9be2f
commit
7c86e3d496
2 changed files with 130 additions and 82 deletions
|
|
@ -7,7 +7,7 @@ use iced::{
|
||||||
};
|
};
|
||||||
use rfd::{AsyncFileDialog, FileHandle};
|
use rfd::{AsyncFileDialog, FileHandle};
|
||||||
|
|
||||||
use crate::db;
|
use crate::db::{self, EntrySearchExt};
|
||||||
|
|
||||||
type Element<'a> = iced::Element<'a, Message, iced::Theme, iced::Renderer>;
|
type Element<'a> = iced::Element<'a, Message, iced::Theme, iced::Renderer>;
|
||||||
|
|
||||||
|
|
@ -22,13 +22,15 @@ enum Message {
|
||||||
OpenDb,
|
OpenDb,
|
||||||
DbOpened(Result<keepass::Database, String>),
|
DbOpened(Result<keepass::Database, String>),
|
||||||
|
|
||||||
SelectionChanged(usize, db::BasicField),
|
ChangeSelection(usize, db::BasicField),
|
||||||
|
|
||||||
SelectionLeft,
|
SelectionLeft,
|
||||||
SelectionDown,
|
SelectionDown,
|
||||||
SelectionUp,
|
SelectionUp,
|
||||||
SelectionRight,
|
SelectionRight,
|
||||||
|
|
||||||
|
ChangeQuery(String),
|
||||||
|
|
||||||
Copy,
|
Copy,
|
||||||
|
|
||||||
FocusNext,
|
FocusNext,
|
||||||
|
|
@ -42,9 +44,11 @@ struct State {
|
||||||
|
|
||||||
db: Option<keepass::Database>,
|
db: Option<keepass::Database>,
|
||||||
|
|
||||||
|
db_loaded: bool,
|
||||||
db_view: Vec<keepass::db::Entry>,
|
db_view: Vec<keepass::db::Entry>,
|
||||||
selected_entry: usize,
|
selected_entry: usize,
|
||||||
selected_field: db::BasicField,
|
selected_field: db::BasicField,
|
||||||
|
query: String,
|
||||||
|
|
||||||
password: String,
|
password: String,
|
||||||
|
|
||||||
|
|
@ -105,14 +109,15 @@ impl State {
|
||||||
.iter_all_entries()
|
.iter_all_entries()
|
||||||
.map(|entry_ref| entry_ref.clone())
|
.map(|entry_ref| entry_ref.clone())
|
||||||
.collect();
|
.collect();
|
||||||
self.db = Some(db)
|
self.db = Some(db);
|
||||||
|
self.db_loaded = true;
|
||||||
}
|
}
|
||||||
Err(e) => self.error = e.into(),
|
Err(e) => self.error = e.into(),
|
||||||
}
|
}
|
||||||
self.status = "".into();
|
self.status = "".into();
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::SelectionChanged(i, field) => {
|
Message::ChangeSelection(i, field) => {
|
||||||
self.selected_entry = i;
|
self.selected_entry = i;
|
||||||
self.selected_field = field;
|
self.selected_field = field;
|
||||||
return Task::done(Message::Copy);
|
return Task::done(Message::Copy);
|
||||||
|
|
@ -132,18 +137,37 @@ impl State {
|
||||||
}
|
}
|
||||||
Message::SelectionRight => self.selected_field.next(),
|
Message::SelectionRight => self.selected_field.next(),
|
||||||
|
|
||||||
|
Message::ChangeQuery(str) => {
|
||||||
|
self.query = str;
|
||||||
|
self.db_view = self
|
||||||
|
.db
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.iter_all_entries()
|
||||||
|
.filter_map(|entry_ref| {
|
||||||
|
entry_ref
|
||||||
|
.contains(&self.query)
|
||||||
|
.then(|| entry_ref.to_owned())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
Message::Copy => {
|
Message::Copy => {
|
||||||
let entry_ref = &self.db_view[self.selected_entry];
|
if let Some(entry_ref) = &self.db_view.get(self.selected_entry) {
|
||||||
if let Some(str) = match self.selected_field {
|
if let Some(str) = match self.selected_field {
|
||||||
db::BasicField::Title => entry_ref.get_password(),
|
db::BasicField::Title => entry_ref.get_password(),
|
||||||
db::BasicField::Username => entry_ref.get_username(),
|
db::BasicField::Username => entry_ref.get_username(),
|
||||||
db::BasicField::Password => entry_ref.get_password(),
|
db::BasicField::Password => entry_ref.get_password(),
|
||||||
db::BasicField::Url => entry_ref.get_url(),
|
db::BasicField::Url => entry_ref.get_url(),
|
||||||
} {
|
} {
|
||||||
|
self.error = "".into();
|
||||||
return clipboard::write(str.to_owned());
|
return clipboard::write(str.to_owned());
|
||||||
} else {
|
} else {
|
||||||
self.error = "nothing to copy!".into();
|
self.error = "nothing to copy!".into();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self.error = "nothing selected!".into();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::FocusNext => return widget::operation::focus_next(),
|
Message::FocusNext => return widget::operation::focus_next(),
|
||||||
|
|
@ -153,7 +177,11 @@ impl State {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
fn view(&self) -> Element<'_> {
|
fn view(&self) -> Element<'_> {
|
||||||
let main = if !self.db_view.is_empty() {
|
let main = if self.db_loaded {
|
||||||
|
widget::column([
|
||||||
|
widget::text_input("search query", &self.query)
|
||||||
|
.on_input(Message::ChangeQuery)
|
||||||
|
.into(),
|
||||||
widget::scrollable(
|
widget::scrollable(
|
||||||
widget::grid(self.db_view.iter().enumerate().flat_map(|(i, entry)| {
|
widget::grid(self.db_view.iter().enumerate().flat_map(|(i, entry)| {
|
||||||
[
|
[
|
||||||
|
|
@ -161,13 +189,14 @@ impl State {
|
||||||
let field = db::BasicField::Title;
|
let field = db::BasicField::Title;
|
||||||
widget::button(widget::text(title))
|
widget::button(widget::text(title))
|
||||||
.style(
|
.style(
|
||||||
if i == self.selected_entry && field == self.selected_field {
|
if i == self.selected_entry && field == self.selected_field
|
||||||
|
{
|
||||||
widget::button::primary
|
widget::button::primary
|
||||||
} else {
|
} else {
|
||||||
widget::button::subtle
|
widget::button::subtle
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on_press(Message::SelectionChanged(i, field))
|
.on_press(Message::ChangeSelection(i, field))
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
widget::space().into()
|
widget::space().into()
|
||||||
|
|
@ -176,13 +205,14 @@ impl State {
|
||||||
let field = db::BasicField::Username;
|
let field = db::BasicField::Username;
|
||||||
widget::button(widget::text(title))
|
widget::button(widget::text(title))
|
||||||
.style(
|
.style(
|
||||||
if i == self.selected_entry && field == self.selected_field {
|
if i == self.selected_entry && field == self.selected_field
|
||||||
|
{
|
||||||
widget::button::primary
|
widget::button::primary
|
||||||
} else {
|
} else {
|
||||||
widget::button::subtle
|
widget::button::subtle
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on_press(Message::SelectionChanged(i, field))
|
.on_press(Message::ChangeSelection(i, field))
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
widget::space().into()
|
widget::space().into()
|
||||||
|
|
@ -191,13 +221,14 @@ impl State {
|
||||||
let field = db::BasicField::Password;
|
let field = db::BasicField::Password;
|
||||||
widget::button(widget::text("[password hidden]"))
|
widget::button(widget::text("[password hidden]"))
|
||||||
.style(
|
.style(
|
||||||
if i == self.selected_entry && field == self.selected_field {
|
if i == self.selected_entry && field == self.selected_field
|
||||||
|
{
|
||||||
widget::button::primary
|
widget::button::primary
|
||||||
} else {
|
} else {
|
||||||
widget::button::subtle
|
widget::button::subtle
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on_press(Message::SelectionChanged(i, field))
|
.on_press(Message::ChangeSelection(i, field))
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
widget::space().into()
|
widget::space().into()
|
||||||
|
|
@ -206,13 +237,14 @@ impl State {
|
||||||
let field = db::BasicField::Url;
|
let field = db::BasicField::Url;
|
||||||
widget::button(widget::text(title))
|
widget::button(widget::text(title))
|
||||||
.style(
|
.style(
|
||||||
if i == self.selected_entry && field == self.selected_field {
|
if i == self.selected_entry && field == self.selected_field
|
||||||
|
{
|
||||||
widget::button::primary
|
widget::button::primary
|
||||||
} else {
|
} else {
|
||||||
widget::button::subtle
|
widget::button::subtle
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on_press(Message::SelectionChanged(i, field))
|
.on_press(Message::ChangeSelection(i, field))
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
widget::space().into()
|
widget::space().into()
|
||||||
|
|
@ -222,6 +254,11 @@ impl State {
|
||||||
.height(Length::Shrink)
|
.height(Length::Shrink)
|
||||||
.columns(4),
|
.columns(4),
|
||||||
)
|
)
|
||||||
|
// I don't know why this needs to be here,
|
||||||
|
// but without it the search bar isn't shown
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
widget::container(
|
widget::container(
|
||||||
|
|
|
||||||
11
src/db.rs
11
src/db.rs
|
|
@ -27,6 +27,17 @@ impl BasicField {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait EntrySearchExt {
|
||||||
|
fn contains(&self, s: impl AsRef<str>) -> bool;
|
||||||
|
}
|
||||||
|
impl EntrySearchExt for keepass::db::EntryRef<'_> {
|
||||||
|
fn contains(&self, s: impl AsRef<str>) -> bool {
|
||||||
|
let str = s.as_ref();
|
||||||
|
self.get_title().is_some_and(|title| title.contains(str))
|
||||||
|
|| self.get_url().is_some_and(|url| url.contains(str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_last_opened() -> io::Result<String> {
|
pub fn get_last_opened() -> io::Result<String> {
|
||||||
Ok(fs::read_to_string(
|
Ok(fs::read_to_string(
|
||||||
dirs::data_dir()
|
dirs::data_dir()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue