diff --git a/i18n/en/cosmic_ext_applet_clipboard_manager.ftl b/i18n/en/cosmic_ext_applet_clipboard_manager.ftl index 6507c30..dd445f5 100644 --- a/i18n/en/cosmic_ext_applet_clipboard_manager.ftl +++ b/i18n/en/cosmic_ext_applet_clipboard_manager.ftl @@ -4,4 +4,5 @@ incognito = Incognito clear_entries = Clear show_qr_code = Show QR code return_to_clipboard = Return to clipboard -qr_code_error = Error while generating the QR code \ No newline at end of file +qr_code_error = Error while generating the QR code +horizontal_layout = Horizontal \ No newline at end of file diff --git a/i18n/fr/cosmic_ext_applet_clipboard_manager.ftl b/i18n/fr/cosmic_ext_applet_clipboard_manager.ftl index 8c88a16..56bbfc8 100644 --- a/i18n/fr/cosmic_ext_applet_clipboard_manager.ftl +++ b/i18n/fr/cosmic_ext_applet_clipboard_manager.ftl @@ -4,4 +4,5 @@ incognito = Incognito clear_entries = Nettoyer show_qr_code = Afficher le code QR return_to_clipboard = Retourner au presse-papier -qr_code_error = Erreur pendant la génération du code QR \ No newline at end of file +qr_code_error = Erreur pendant la génération du code QR +horizontal_layout = Horizontal \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index f8700e1..2e42058 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,12 @@ use cosmic::app::{command, Core}; use cosmic::iced::advanced::subscription; +use cosmic::iced::keyboard::key::Named; +use cosmic::iced::wayland::actions::layer_surface::{IcedMargin, SctkLayerSurfaceSettings}; +use cosmic::iced::wayland::actions::popup::SctkPopupSettings; +use cosmic::iced::wayland::layer_surface::{ + destroy_layer_surface, get_layer_surface, Anchor, KeyboardInteractivity, +}; use cosmic::iced::wayland::popup::{destroy_popup, get_popup}; use cosmic::iced::window::Id; use cosmic::iced::{self, event, Command, Limits}; @@ -17,9 +23,9 @@ use futures::executor::block_on; use crate::config::{Config, CONFIG_VERSION, PRIVATE_MODE}; use crate::db::{self, Db, Entry}; -use crate::message::AppMessage; +use crate::message::{AppMsg, ConfigMsg}; +use crate::navigation::NavigationMessage; use crate::utils::command_message; -use crate::view::{popup_view, quick_settings_view}; use crate::{clipboard, config, navigation}; use cosmic::cosmic_config; @@ -31,15 +37,11 @@ pub const ORG: &str = "wiiznokes"; pub const APP: &str = "cosmic-ext-applet-clipboard-manager"; pub const APPID: &str = constcat::concat!(QUALIFIER, ".", ORG, ".", APP); -pub struct Window { +pub struct AppState { core: Core, - config: Config, config_handler: cosmic_config::Config, popup: Option, - state: AppState, -} - -pub struct AppState { + pub config: Config, pub db: Db, pub clipboard_state: ClipboardState, pub focused: usize, @@ -87,9 +89,9 @@ enum PopupKind { QuickSettings, } -impl Window { - fn toggle_popup(&mut self, kind: PopupKind) -> Command> { - self.state.qr_code.take(); +impl AppState { + fn toggle_popup(&mut self, kind: PopupKind) -> Command> { + self.qr_code.take(); match &self.popup { Some(popup) => { if popup.kind == kind { @@ -102,40 +104,62 @@ impl Window { } } - fn close_popup(&mut self) -> Command> { - self.state.focused = 0; - self.state.db.set_query_and_search("".into()); + fn close_popup(&mut self) -> Command> { + self.focused = 0; + self.db.set_query_and_search("".into()); if let Some(popup) = self.popup.take() { //info!("destroy {:?}", popup.id); - destroy_popup(popup.id) + + if self.config.horizontal { + destroy_layer_surface(popup.id) + } else { + destroy_popup(popup.id) + } } else { Command::none() } } - fn open_popup(&mut self, kind: PopupKind) -> Command> { + fn open_popup(&mut self, kind: PopupKind) -> Command> { let new_id = Id::unique(); //info!("will create {:?}", new_id); let popup = Popup { kind, id: new_id }; - self.popup.replace(popup); - let mut popup_settings = - self.core - .applet - .get_popup_settings(Id::MAIN, new_id, None, None, None); match kind { PopupKind::Popup => { - popup_settings.positioner.size_limits = Limits::NONE - .max_width(400.0) - .min_width(300.0) - .min_height(200.0) - .max_height(500.0); - get_popup(popup_settings) + if self.config.horizontal { + get_layer_surface(SctkLayerSurfaceSettings { + id: new_id, + keyboard_interactivity: KeyboardInteractivity::OnDemand, + anchor: Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT, + namespace: "clipboard manager".into(), + size: Some((None, Some(350))), + size_limits: Limits::NONE.min_width(1.0).min_height(1.0), + ..Default::default() + }) + } else { + let mut popup_settings = + self.core + .applet + .get_popup_settings(Id::MAIN, new_id, None, None, None); + + popup_settings.positioner.size_limits = Limits::NONE + .max_width(400.0) + .min_width(300.0) + .min_height(200.0) + .max_height(500.0); + get_popup(popup_settings) + } } PopupKind::QuickSettings => { + let mut popup_settings = + self.core + .applet + .get_popup_settings(Id::MAIN, new_id, None, None, None); + popup_settings.positioner.size_limits = Limits::NONE .max_width(250.0) .min_width(200.0) @@ -148,10 +172,10 @@ impl Window { } } -impl cosmic::Application for Window { +impl cosmic::Application for AppState { type Executor = cosmic::executor::Default; type Flags = Flags; - type Message = AppMessage; + type Message = AppMsg; const APP_ID: &'static str = APPID; fn core(&self) -> &Core { @@ -171,22 +195,20 @@ impl cosmic::Application for Window { let db = block_on(async { db::Db::new(&config).await.unwrap() }); - let window = Window { + let window = AppState { core, config_handler: flags.config_handler, popup: None, - state: AppState { - db, - clipboard_state: ClipboardState::Init, - focused: 0, - qr_code: None, - }, + db, + clipboard_state: ClipboardState::Init, + focused: 0, + qr_code: None, config, }; #[cfg(debug_assertions)] let command = Command::single(Action::Future(Box::pin(async { - cosmic::app::Message::App(AppMessage::TogglePopup) + cosmic::app::Message::App(AppMsg::TogglePopup) }))); #[cfg(not(debug_assertions))] @@ -195,12 +217,12 @@ impl cosmic::Application for Window { (window, command) } - fn on_close_requested(&self, id: window::Id) -> Option { + fn on_close_requested(&self, id: window::Id) -> Option { info!("on_close_requested"); if let Some(popup) = &self.popup { if popup.id == id { - return Some(AppMessage::ClosePopup); + return Some(AppMsg::ClosePopup); } } None @@ -219,78 +241,91 @@ impl cosmic::Application for Window { } match message { - AppMessage::ChangeConfig(config) => { + AppMsg::ChangeConfig(config) => { if config != self.config { PRIVATE_MODE.store(config.private_mode, atomic::Ordering::Relaxed); self.config = config; } } - AppMessage::ToggleQuickSettings => { + AppMsg::ToggleQuickSettings => { return self.toggle_popup(PopupKind::QuickSettings); } - AppMessage::TogglePopup => { + AppMsg::TogglePopup => { return self.toggle_popup(PopupKind::Popup); } - AppMessage::ClosePopup => return self.close_popup(), - AppMessage::Search(query) => { - self.state.db.set_query_and_search(query); + AppMsg::ClosePopup => return self.close_popup(), + AppMsg::Search(query) => { + self.db.set_query_and_search(query); } - AppMessage::ClipboardEvent(message) => match message { + AppMsg::ClipboardEvent(message) => match message { clipboard::ClipboardMessage::Connected => { - self.state.clipboard_state = ClipboardState::Connected; + self.clipboard_state = ClipboardState::Connected; } clipboard::ClipboardMessage::Data(data) => { block_on(async { - if let Err(e) = self.state.db.insert(data).await { + if let Err(e) = self.db.insert(data).await { error!("can't insert data: {e}"); } }); } clipboard::ClipboardMessage::Error(e) => { error!("{e}"); - self.state.clipboard_state = ClipboardState::Error(e); + self.clipboard_state = ClipboardState::Error(e); } clipboard::ClipboardMessage::EmptyKeyboard => { - if let Some(data) = self.state.db.get(0) { + if let Some(data) = self.db.get(0) { if let Err(e) = clipboard::copy(data.to_owned()) { error!("can't copy: {e}"); } } } }, - AppMessage::Copy(data) => { + AppMsg::Copy(data) => { if let Err(e) = clipboard::copy(data) { error!("can't copy: {e}"); } return self.close_popup(); } - AppMessage::Delete(data) => { + AppMsg::Delete(data) => { block_on(async { - if let Err(e) = self.state.db.delete(&data).await { + if let Err(e) = self.db.delete(&data).await { error!("can't delete {:?}: {}", data.get_content(), e); } }); } - AppMessage::Clear => { + AppMsg::Clear => { block_on(async { - if let Err(e) = self.state.db.clear().await { + if let Err(e) = self.db.clear().await { error!("can't clear db: {e}"); } }); } - AppMessage::RetryConnectingClipboard => { - self.state.clipboard_state = ClipboardState::Init; + AppMsg::RetryConnectingClipboard => { + self.clipboard_state = ClipboardState::Init; } - AppMessage::Navigation(message) => match message { + AppMsg::Navigation(message) => match message { + navigation::NavigationMessage::Event(e) => { + let message = match e { + Named::Enter => NavigationMessage::Enter, + Named::Escape => NavigationMessage::Quit, + Named::ArrowDown if !self.config.horizontal => NavigationMessage::Next, + Named::ArrowUp if !self.config.horizontal => NavigationMessage::Previous, + Named::ArrowLeft if self.config.horizontal => NavigationMessage::Previous, + Named::ArrowRight if self.config.horizontal => NavigationMessage::Next, + _ => NavigationMessage::None, + }; + + return command_message(AppMsg::Navigation(message)); + } navigation::NavigationMessage::Next => { - self.state.focus_next(); + self.focus_next(); } navigation::NavigationMessage::Previous => { - self.state.focus_previous(); + self.focus_previous(); } navigation::NavigationMessage::Enter => { - if let Some(data) = self.state.db.get(self.state.focused) { + if let Some(data) = self.db.get(self.focused) { if let Err(e) = clipboard::copy(data.clone()) { error!("can't copy: {e}"); } @@ -300,38 +335,44 @@ impl cosmic::Application for Window { navigation::NavigationMessage::Quit => { return self.close_popup(); } + NavigationMessage::None => {} }, - AppMessage::PrivateMode(private_mode) => { - config_set!(private_mode, private_mode); - PRIVATE_MODE.store(private_mode, atomic::Ordering::Relaxed); - } - AppMessage::Db(inner) => { + AppMsg::Db(inner) => { block_on(async { - if let Err(err) = self.state.db.handle_message(inner).await { + if let Err(err) = self.db.handle_message(inner).await { error!("{err}"); } }); } - AppMessage::ShowQrCode(e) => { + AppMsg::ShowQrCode(e) => { // todo: handle better this error if e.content.len() < 700 { match qr_code::State::new(&e.content) { Ok(s) => { - self.state.qr_code.replace(Ok(s)); + self.qr_code.replace(Ok(s)); } Err(e) => { error!("{e}"); - self.state.qr_code.replace(Err(())); + self.qr_code.replace(Err(())); } } } else { error!("qr code to long: {}", e.content.len()); - self.state.qr_code.replace(Err(())); + self.qr_code.replace(Err(())); } } - AppMessage::ReturnToClipboard => { - self.state.qr_code.take(); + AppMsg::ReturnToClipboard => { + self.qr_code.take(); } + AppMsg::Config(msg) => match msg { + ConfigMsg::PrivateMode(private_mode) => { + config_set!(private_mode, private_mode); + PRIVATE_MODE.store(private_mode, atomic::Ordering::Relaxed); + } + ConfigMsg::Horizontal(horizontal) => { + config_set!(horizontal, horizontal); + } + }, } Command::none() } @@ -341,25 +382,21 @@ impl cosmic::Application for Window { .core .applet .icon_button(constcat::concat!(APPID, "-symbolic")) - .on_press(AppMessage::TogglePopup); + .on_press(AppMsg::TogglePopup); MouseArea::new(icon) - .on_right_release(AppMessage::ToggleQuickSettings) + .on_right_release(AppMsg::ToggleQuickSettings) .into() } fn view_window(&self, _id: Id) -> Element { let Some(popup) = &self.popup else { - return self - .core - .applet - .popup_container(popup_view(&self.state, &self.config)) - .into(); + return self.core.applet.popup_container(self.popup_view()).into(); }; let view = match &popup.kind { - PopupKind::Popup => popup_view(&self.state, &self.config), - PopupKind::QuickSettings => quick_settings_view(&self.state, &self.config), + PopupKind::Popup => self.popup_view(), + PopupKind::QuickSettings => self.quick_settings_view(), }; self.core.applet.popup_container(view).into() @@ -367,12 +404,12 @@ impl cosmic::Application for Window { fn subscription(&self) -> Subscription { let mut subscriptions = vec![ config::sub(), - navigation::sub().map(AppMessage::Navigation), - db::sub().map(AppMessage::Db), + navigation::sub().map(AppMsg::Navigation), + db::sub().map(AppMsg::Db), ]; - if !self.state.clipboard_state.is_error() { - subscriptions.push(clipboard::sub().map(AppMessage::ClipboardEvent)); + if !self.clipboard_state.is_error() { + subscriptions.push(clipboard::sub().map(AppMsg::ClipboardEvent)); } Subscription::batch(subscriptions) diff --git a/src/config.rs b/src/config.rs index 5b4fc16..fb77ad4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,7 @@ use cosmic::{ use serde::{Deserialize, Serialize}; -use crate::{app::APPID, message::AppMessage, utils}; +use crate::{app::APPID, message::AppMsg, utils}; pub const CONFIG_VERSION: u64 = 2; @@ -16,6 +16,7 @@ pub struct Config { pub private_mode: bool, pub maximum_entries_lifetime: Option, pub maximum_entries_number: Option, + pub horizontal: bool, } impl Default for Config { @@ -24,13 +25,14 @@ impl Default for Config { private_mode: false, maximum_entries_lifetime: Some(Duration::from_secs(30 * 24 * 60 * 60)), // 30 days, maximum_entries_number: Some(500), + horizontal: false, } } } pub static PRIVATE_MODE: AtomicBool = AtomicBool::new(false); -pub fn sub() -> Subscription { +pub fn sub() -> Subscription { struct ConfigSubscription; cosmic_config::config_subscription( @@ -42,6 +44,6 @@ pub fn sub() -> Subscription { if !update.errors.is_empty() { error!("can't load config {:?}: {:?}", update.keys, update.errors); } - AppMessage::ChangeConfig(update.config) + AppMsg::ChangeConfig(update.config) }) } diff --git a/src/db.rs b/src/db.rs index a4b4069..ab1be32 100644 --- a/src/db.rs +++ b/src/db.rs @@ -34,7 +34,7 @@ use mime::Mime; use crate::{ app::{APP, APPID, ORG, QUALIFIER}, config::Config, - message::AppMessage, + message::AppMsg, utils::{self, now_millis, remove_dir_contents}, }; diff --git a/src/main.rs b/src/main.rs index 83e9524..c434756 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ #![allow(unused_macros)] #![allow(unused_imports)] -use app::{Flags, Window}; +use app::{AppState, Flags}; use config::{Config, CONFIG_VERSION}; use cosmic::cosmic_config; use cosmic::cosmic_config::CosmicConfigEntry; @@ -70,5 +70,5 @@ fn main() -> cosmic::iced::Result { config_handler, config, }; - cosmic::applet::run::(true, flags) + cosmic::applet::run::(true, flags) } diff --git a/src/message.rs b/src/message.rs index b3558e8..daf8b1d 100644 --- a/src/message.rs +++ b/src/message.rs @@ -8,7 +8,7 @@ use crate::{ }; #[derive(Clone, Debug)] -pub enum AppMessage { +pub enum AppMsg { ChangeConfig(Config), TogglePopup, ToggleQuickSettings, @@ -18,10 +18,16 @@ pub enum AppMessage { RetryConnectingClipboard, Copy(Entry), Delete(Entry), - PrivateMode(bool), Clear, Navigation(NavigationMessage), Db(DbMessage), ShowQrCode(Entry), ReturnToClipboard, + Config(ConfigMsg), +} + +#[derive(Clone, Debug)] +pub enum ConfigMsg { + PrivateMode(bool), + Horizontal(bool), } diff --git a/src/navigation.rs b/src/navigation.rs index 12692fe..0aff0ba 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -6,6 +6,8 @@ pub enum NavigationMessage { Previous, Enter, Quit, + Event(cosmic::iced::keyboard::key::Named), + None, } #[allow(clippy::collapsible_match)] @@ -19,19 +21,13 @@ pub fn sub() -> Subscription { cosmic::iced::keyboard::Event::KeyPressed { key, .. } => { match key { cosmic::iced::keyboard::Key::Named(named) => match named { - cosmic::iced::keyboard::key::Named::Enter => { - Some(NavigationMessage::Enter) - } - - cosmic::iced::keyboard::key::Named::Escape => { - Some(NavigationMessage::Quit) - } - - cosmic::iced::keyboard::key::Named::ArrowDown => { - Some(NavigationMessage::Next) - } - cosmic::iced::keyboard::key::Named::ArrowUp => { - Some(NavigationMessage::Previous) + cosmic::iced::keyboard::key::Named::Enter + | cosmic::iced::keyboard::key::Named::Escape + | cosmic::iced::keyboard::key::Named::ArrowDown + | cosmic::iced::keyboard::key::Named::ArrowUp + | cosmic::iced::keyboard::key::Named::ArrowLeft + | cosmic::iced::keyboard::key::Named::ArrowRight => { + Some(NavigationMessage::Event(named)) } /* diff --git a/src/utils.rs b/src/utils.rs index 448e4ba..9867437 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,7 @@ use cosmic::{app::Message, iced::Padding, iced_runtime::command::Action, Command use crate::app::APPID; -pub fn formated_value(value: &str, max_lines: usize, max_chars: usize) -> Cow { +pub fn formatted_value(value: &str, max_lines: usize, max_chars: usize) -> Cow { let value = value.trim(); if value.lines().count() <= max_lines && value.len() <= max_chars { diff --git a/src/view.rs b/src/view.rs index 4c715ce..cbc0e04 100644 --- a/src/view.rs +++ b/src/view.rs @@ -2,7 +2,12 @@ use std::{borrow::Cow, cmp::min, path::PathBuf}; use cosmic::{ iced::{Alignment, Length, Padding}, - iced_widget::{graphics::image::image_rs::flat::View, qr_code, QRCode, Row, Scrollable}, + iced_widget::{ + graphics::image::image_rs::flat::View, + qr_code, + scrollable::{Direction, Properties}, + QRCode, Row, Scrollable, + }, prelude::CollectionWidget, theme::{self, Button}, widget::{ @@ -23,266 +28,324 @@ use crate::{ config::Config, db::{Content, Entry}, fl, - message::AppMessage, - utils::{formated_value, horizontal_padding}, + message::{AppMsg, ConfigMsg}, + utils::{formatted_value, horizontal_padding, vertical_padding}, }; -pub fn quick_settings_view<'a>( - _state: &'a AppState, - config: &'a Config, -) -> Element<'a, AppMessage> { - fn toogle_settings<'a>( - info: impl Into> + 'a, - value: bool, - f: impl Fn(bool) -> AppMessage + 'a, - ) -> Element<'a, AppMessage> { - Row::new() - .push(text(info)) - .push(Space::with_width(Length::Fill)) - .push(toggler(None, value, f)) +impl AppState { + pub fn quick_settings_view(&self) -> Element<'_, AppMsg> { + fn toggle_settings<'a>( + info: impl Into> + 'a, + value: bool, + f: impl Fn(bool) -> AppMsg + 'a, + ) -> Element<'a, AppMsg> { + Row::new() + .push(text(info)) + .push(Space::with_width(Length::Fill)) + .push(toggler(None, value, f)) + .into() + } + + Column::new() + .width(Length::Fill) + .spacing(20) + .padding(10) + .push(toggle_settings( + fl!("incognito"), + self.config.private_mode, + |v| AppMsg::Config(ConfigMsg::PrivateMode(v)), + )) + .push(toggle_settings( + fl!("horizontal_layout"), + self.config.horizontal, + |v| AppMsg::Config(ConfigMsg::Horizontal(v)), + )) + .push(widget::button::destructive(fl!("clear_entries")).on_press(AppMsg::Clear)) .into() } - Column::new() - .width(Length::Fill) - .spacing(20) - .padding(10) - .push(toogle_settings( - fl!("incognito"), - config.private_mode, - AppMessage::PrivateMode, - )) - .push(widget::button::destructive(fl!("clear_entries")).on_press(AppMessage::Clear)) - .into() -} + pub fn popup_view(&self) -> Element<'_, AppMsg> { + Column::new() + .push(self.top_bar()) + .push(self.content()) + .width(Length::Fill) + .spacing(20) + .padding(10) + .align_items(Alignment::Center) + .into() + } -#[inline] -fn top_bar<'a>(ele: impl Into>) -> Element<'a, AppMessage> { - let mut padding = Padding::new(10f32); - padding.bottom = 0f32; + fn top_bar(&self) -> Element<'_, AppMsg> { + let content: Element<_> = match self.qr_code.is_none() { + true => text_input::search_input(fl!("search_entries"), self.db.query()) + .always_active() + .on_input(AppMsg::Search) + .on_paste(AppMsg::Search) + .on_clear(AppMsg::Search("".into())) + .width(match self.config.horizontal { + true => Length::Fixed(250f32), + false => Length::Fill, + }) + .into(), + false => button::text(fl!("return_to_clipboard")) + .on_press(AppMsg::ReturnToClipboard) + .width(match self.config.horizontal { + true => Length::Shrink, + false => Length::Fill, + }) + .into(), + }; - container(ele).padding(padding).into() -} + let mut padding = Padding::new(10f32); + padding.bottom = 0f32; -pub fn popup_view<'a>(state: &'a AppState, _config: &'a Config) -> Element<'a, AppMessage> { - let mut col = Column::new().width(Length::Fill).spacing(20).padding(10); + let content = container(content).padding(padding); - match &state.qr_code { - Some(qr_code) => { - let qr_code_content: Element<_> = match qr_code { - Ok(c) => QRCode::new(c).into(), - Err(()) => text(fl!("qr_code_error")).into(), - }; + content.into() + } - col = col - .push(top_bar( - button::text(fl!("return_to_clipboard")) - .width(Length::Fill) - .on_press(AppMessage::ReturnToClipboard), - )) - .push( - container(qr_code_content) - .width(Length::Fill) + fn content(&self) -> Element<'_, AppMsg> { + match &self.qr_code { + Some(qr_code) => { + let qr_code_content: Element<_> = match qr_code { + Ok(c) => QRCode::new(c).into(), + Err(()) => text(fl!("qr_code_error")).into(), + }; + + return container(qr_code_content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into(); + } + None => { + let entries_view: Vec<_> = if self.db.query().is_empty() { + self.db + .iter() + .enumerate() + .filter_map(|(pos, data)| match data.get_content() { + Ok(c) => match c { + Content::Text(text) => { + self.text_entry(data, pos == self.focused, text) + } + Content::Image(image) => { + self.image_entry(data, pos == self.focused, image) + } + Content::UriList(uris) => { + self.uris_entry(data, pos == self.focused, &uris) + } + }, + Err(_) => None, + }) + .collect() + } else { + self.db + .search_iter() + .enumerate() + .filter_map(|(pos, (data, indices))| match data.get_content() { + Ok(c) => match c { + Content::Text(text) => self.text_entry_with_indices( + data, + pos == self.focused, + text, + indices, + ), + Content::Image(image) => { + self.image_entry(data, pos == self.focused, image) + } + Content::UriList(uris) => { + self.uris_entry(data, pos == self.focused, &uris) + } + }, + Err(_) => None, + }) + .collect() + }; + + if self.config.horizontal { + // try to fix scroll bar + let padding = Padding { + top: 0f32, + right: 10f32, + bottom: 20f32, + left: 10f32, + }; + + let column = row::with_children(entries_view) + .spacing(5f32) + .padding(padding); + + Scrollable::new(column) + .direction(Direction::Horizontal(Properties::default())) + .into() + } else { + // try to fix scroll bar + let padding = Padding { + top: 0f32, + right: 20f32, + bottom: 0f32, + left: 10f32, + }; + + let column = column::with_children(entries_view) + .spacing(5f32) + .padding(padding); + + Scrollable::new(column) + // XXX: why ? .height(Length::FillPortion(2)) - .center_x() - .center_y(), - ); - } - None => { - col = col - .push(top_bar( - text_input::search_input(fl!("search_entries"), state.db.query()) - .always_active() - .on_input(AppMessage::Search) - .on_paste(AppMessage::Search) - .on_clear(AppMessage::Search("".into())), - )) - .push(entries(state)); + .into() + } + } } } - col.into() -} + fn image_entry<'a>( + &'a self, + entry: &'a Entry, + is_focused: bool, + image_data: &'a [u8], + ) -> Option> { + let handle = image::Handle::from_memory(image_data.to_owned()); -fn entries(state: &AppState) -> Element<'_, AppMessage> { - let entries_view: Vec<_> = if state.db.query().is_empty() { - state - .db - .iter() - .enumerate() - .filter_map(|(pos, data)| match data.get_content() { - Ok(c) => match c { - Content::Text(text) => text_entry(data, pos == state.focused, text), - Content::Image(image) => image_entry(data, pos == state.focused, image), - Content::UriList(uris) => uris_entry(data, pos == state.focused, &uris), - }, - Err(_) => None, - }) - .collect() - } else { - state - .db - .search_iter() - .enumerate() - .filter_map(|(pos, (data, indices))| match data.get_content() { - Ok(c) => match c { - Content::Text(text) => { - text_entry_with_indices(data, pos == state.focused, text, indices) - } - Content::Image(image) => image_entry(data, pos == state.focused, image), - Content::UriList(uris) => uris_entry(data, pos == state.focused, &uris), - }, - Err(_) => None, - }) - .collect() - }; - - let mut padding = horizontal_padding(10f32); - // try to fix scroll bar - padding.right += 10f32; - - let column = column::with_children(entries_view) - .spacing(5f32) - .padding(padding); - - Scrollable::new(column) - .height(Length::FillPortion(2)) - .into() -} + Some(self.base_entry(entry, is_focused, image(handle).width(Length::Fill))) + } -fn image_entry<'a>( - entry: &'a Entry, - is_focused: bool, - image_data: &'a [u8], -) -> Option> { - let handle = image::Handle::from_memory(image_data.to_owned()); - - Some(base_entry( - entry, - is_focused, - image(handle).width(Length::Fill), - )) -} + fn uris_entry<'a>( + &'a self, + entry: &'a Entry, + is_focused: bool, + uris: &[&'a str], + ) -> Option> { + if uris.is_empty() { + return None; + } -fn uris_entry<'a>( - entry: &'a Entry, - is_focused: bool, - uris: &[&'a str], -) -> Option> { - if uris.is_empty() { - return None; - } + let max = 3; - let max = 3; + let mut lines = Vec::with_capacity(min(uris.len(), max + 1)); + + for uri in uris.iter().take(max) { + lines.push(text(*uri).into()); + } - let mut lines = Vec::with_capacity(min(uris.len(), max + 1)); + if uris.len() > max { + lines.push(text("...").into()); + } - for uri in uris.iter().take(max) { - lines.push(text(*uri).into()); + Some(self.base_entry( + entry, + is_focused, + column::with_children(lines).width(Length::Fill), + )) } - if uris.len() > max { - lines.push(text("...").into()); + fn text_entry_with_indices<'a>( + &'a self, + entry: &'a Entry, + is_focused: bool, + content: &'a str, + _indices: &'a [u32], + ) -> Option> { + self.text_entry(entry, is_focused, content) } - Some(base_entry( - entry, - is_focused, - column::with_children(lines).width(Length::Fill), - )) -} + fn text_entry<'a>( + &'a self, + entry: &'a Entry, + is_focused: bool, + content: &'a str, + ) -> Option> { + if content.is_empty() { + return None; + } + // todo: remove this max line things: display the maximum + if self.config.horizontal { + Some(self.base_entry(entry, is_focused, text(formatted_value(content, 10, 500)))) + } else { + Some(self.base_entry(entry, is_focused, text(formatted_value(content, 5, 200)))) + } + } -fn text_entry_with_indices<'a>( - entry: &'a Entry, - is_focused: bool, - content: &'a str, - _indices: &'a [u32], -) -> Option> { - text_entry(entry, is_focused, content) -} + fn base_entry<'a>( + &'a self, + entry: &'a Entry, + is_focused: bool, + content: impl Into>, + ) -> Element<'a, AppMsg> { + let btn = cosmic::widget::button(content) + .on_press(AppMsg::Copy(entry.clone())) + .padding([8, 16]) + .width(Length::Fill) + .height(Length::Fill) + .style(Button::Custom { + active: Box::new(move |focused, theme| { + let rad_s = theme.cosmic().corner_radii.radius_s; + let focused = is_focused || focused; -fn text_entry<'a>( - entry: &'a Entry, - is_focused: bool, - content: &'a str, -) -> Option> { - if content.is_empty() { - return None; - } + let a = if focused { + button::StyleSheet::hovered(theme, focused, focused, &Button::Text) + } else { + button::StyleSheet::active(theme, focused, focused, &Button::Standard) + }; + button::Appearance { + border_radius: rad_s.into(), + outline_width: 0.0, + ..a + } + }), + hovered: Box::new(move |focused, theme| { + let focused = is_focused || focused; + let rad_s = theme.cosmic().corner_radii.radius_s; - Some(base_entry( - entry, - is_focused, - text(formated_value(content, 5, 200)), - )) -} + let text = button::StyleSheet::hovered(theme, focused, focused, &Button::Text); + button::Appearance { + border_radius: rad_s.into(), + outline_width: 0.0, + ..text + } + }), + disabled: Box::new(|theme| button::StyleSheet::disabled(theme, &Button::Text)), + pressed: Box::new(move |focused, theme| { + let focused = is_focused || focused; + let rad_s = theme.cosmic().corner_radii.radius_s; -fn base_entry<'a>( - entry: &'a Entry, - is_focused: bool, - content: impl Into>, -) -> Element<'a, AppMessage> { - let btn = cosmic::widget::button(content) - .width(Length::Fill) - .on_press(AppMessage::Copy(entry.clone())) - .padding([8, 16]) - .style(Button::Custom { - active: Box::new(move |focused, theme| { - let rad_s = theme.cosmic().corner_radii.radius_s; - let focused = is_focused || focused; - - let a = if focused { - button::StyleSheet::hovered(theme, focused, focused, &Button::Text) - } else { - button::StyleSheet::active(theme, focused, focused, &Button::Standard) - }; - button::Appearance { - border_radius: rad_s.into(), - outline_width: 0.0, - ..a - } - }), - hovered: Box::new(move |focused, theme| { - let focused = is_focused || focused; - let rad_s = theme.cosmic().corner_radii.radius_s; - - let text = button::StyleSheet::hovered(theme, focused, focused, &Button::Text); - button::Appearance { - border_radius: rad_s.into(), - outline_width: 0.0, - ..text - } - }), - disabled: Box::new(|theme| button::StyleSheet::disabled(theme, &Button::Text)), - pressed: Box::new(move |focused, theme| { - let focused = is_focused || focused; - let rad_s = theme.cosmic().corner_radii.radius_s; - - let text = button::StyleSheet::pressed(theme, focused, focused, &Button::Text); - button::Appearance { - border_radius: rad_s.into(), - outline_width: 0.0, - ..text - } - }), - }); - - context_menu( - btn, - Some(vec![ - menu::Tree::new( - button(text(fl!("delete_entry"))) - .on_press(AppMessage::Delete(entry.clone())) - .width(Length::Fill) - .style(Button::Destructive), - ), - menu::Tree::new( - button(text(fl!("show_qr_code"))) - .on_press(AppMessage::ShowQrCode(entry.clone())) - .width(Length::Fill) - .style(Button::Destructive), - ), - ]), - ) - .into() + let text = button::StyleSheet::pressed(theme, focused, focused, &Button::Text); + button::Appearance { + border_radius: rad_s.into(), + outline_width: 0.0, + ..text + } + }), + }); + + let btn = container(btn); + + // XXX: min width + let btn = if self.config.horizontal { + btn.height(Length::Fill).max_width(350f32) + } else { + btn.width(Length::Fill) + }; + + context_menu( + btn, + Some(vec![ + menu::Tree::new( + button(text(fl!("delete_entry"))) + .on_press(AppMsg::Delete(entry.clone())) + .width(Length::Fill) + .style(Button::Destructive), + ), + menu::Tree::new( + button(text(fl!("show_qr_code"))) + .on_press(AppMsg::ShowQrCode(entry.clone())) + .width(Length::Fill) + .style(Button::Destructive), + ), + ]), + ) + .into() + } }