implement a more consistent login mask with a stable layout and improved error messages
This commit is contained in:
parent
f7e470ca58
commit
97d69f37e9
5 changed files with 352 additions and 78 deletions
|
|
@ -54,10 +54,14 @@ fn main() {
|
||||||
Request::PostAuthMessageResponse { response } => {
|
Request::PostAuthMessageResponse { response } => {
|
||||||
match response.as_deref() {
|
match response.as_deref() {
|
||||||
Some("password") => Response::Success,
|
Some("password") => Response::Success,
|
||||||
_ => Response::Error {
|
_ => {
|
||||||
error_type: ErrorType::AuthError,
|
// Add 1 second delay to simulate real PAM authentication failure behavior
|
||||||
description: "AUTH_ERR".to_string(),
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
},
|
Response::Error {
|
||||||
|
error_type: ErrorType::AuthError,
|
||||||
|
description: "AUTH_ERR".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Request::StartSession { .. } => Response::Success,
|
Request::StartSession { .. } => Response::Success,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ accessibility = Accessibility
|
||||||
.magnifier = Magnifier
|
.magnifier = Magnifier
|
||||||
.high-contrast = High contrast
|
.high-contrast = High contrast
|
||||||
.invert-colors = Invert Colors
|
.invert-colors = Invert Colors
|
||||||
|
authenticating = Authenticating...
|
||||||
cancel = Cancel
|
cancel = Cancel
|
||||||
caps-lock = Caps Lock is active.
|
caps-lock = Caps Lock is active.
|
||||||
enter-user = Enter name manually...
|
enter-user = Enter name manually...
|
||||||
|
|
@ -25,3 +26,10 @@ shutdown-timeout = The system will shut down automatically in
|
||||||
}
|
}
|
||||||
suspend = Suspend
|
suspend = Suspend
|
||||||
user = User
|
user = User
|
||||||
|
|
||||||
|
# Authentication errors
|
||||||
|
auth-error-default = Authentication failed. Please try again.
|
||||||
|
auth-error-credentials = Incorrect password. Please check your keyboard layout and try again.
|
||||||
|
auth-error-denied = Access denied.
|
||||||
|
auth-error-maxtries = Too many failed authentication attempts.
|
||||||
|
auth-error-account = Account is unavailable or disabled.
|
||||||
|
|
|
||||||
200
src/greeter.rs
200
src/greeter.rs
|
|
@ -410,6 +410,7 @@ pub enum Message {
|
||||||
HighContrast(bool),
|
HighContrast(bool),
|
||||||
InvertColors(bool),
|
InvertColors(bool),
|
||||||
WaylandUpdate(WaylandUpdate),
|
WaylandUpdate(WaylandUpdate),
|
||||||
|
SpinnerTick,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<common::Message> for Message {
|
impl From<common::Message> for Message {
|
||||||
|
|
@ -438,6 +439,9 @@ pub struct App {
|
||||||
randr_list: Option<cosmic_randr_shell::List>,
|
randr_list: Option<cosmic_randr_shell::List>,
|
||||||
|
|
||||||
accessibility: Accessibility,
|
accessibility: Accessibility,
|
||||||
|
authenticating: bool,
|
||||||
|
spinner_rotation: f32,
|
||||||
|
spinner_handle: Option<cosmic::iced::task::Handle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -617,7 +621,7 @@ impl App {
|
||||||
}
|
}
|
||||||
let item_cnt = items.len();
|
let item_cnt = items.len();
|
||||||
let menu_button = widget::menu::menu_button(vec![
|
let menu_button = widget::menu::menu_button(vec![
|
||||||
Element::from(widget::Space::with_width(Length::Fixed(25.0))),
|
Element::from(widget::Space::with_width(Length::Fixed(10.0))),
|
||||||
widget::text(fl!("enter-user"))
|
widget::text(fl!("enter-user"))
|
||||||
.align_x(iced::alignment::Horizontal::Left)
|
.align_x(iced::alignment::Horizontal::Left)
|
||||||
.into(),
|
.into(),
|
||||||
|
|
@ -756,6 +760,21 @@ impl App {
|
||||||
.spacing(12.0)
|
.spacing(12.0)
|
||||||
.max_width(280.0);
|
.max_width(280.0);
|
||||||
|
|
||||||
|
let military_time = self
|
||||||
|
.selected_username
|
||||||
|
.data_idx
|
||||||
|
.and_then(|i| self.flags.user_datas.get(i))
|
||||||
|
.map(|user_data| user_data.time_applet_config.military_time)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let space_height = match military_time {
|
||||||
|
true => 63.0,
|
||||||
|
false => 10.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add top spacing for better visual appearance
|
||||||
|
// Bottom of the password text input field should align with bottom of time widget
|
||||||
|
column = column.push(widget::Space::with_height(Length::Fixed(space_height)));
|
||||||
|
|
||||||
match &self.socket_state {
|
match &self.socket_state {
|
||||||
SocketState::Pending => {
|
SocketState::Pending => {
|
||||||
column = column.push(widget::text("Opening GREETD_SOCK"));
|
column = column.push(widget::text("Opening GREETD_SOCK"));
|
||||||
|
|
@ -769,16 +788,32 @@ impl App {
|
||||||
{
|
{
|
||||||
if !self.entering_name && user_data.name == self.selected_username.username
|
if !self.entering_name && user_data.name == self.selected_username.username
|
||||||
{
|
{
|
||||||
if let Some(icon) = user_icon {
|
// Display user icon or empty transparent box
|
||||||
|
if let Some(icon_handle) = user_icon {
|
||||||
column = column.push(
|
column = column.push(
|
||||||
widget::container(
|
widget::container(
|
||||||
widget::image(icon)
|
widget::image(icon_handle)
|
||||||
.width(Length::Fixed(78.0))
|
.width(Length::Fixed(78.0))
|
||||||
.height(Length::Fixed(78.0)),
|
.height(Length::Fixed(78.0))
|
||||||
|
.content_fit(iced::ContentFit::Fill),
|
||||||
)
|
)
|
||||||
|
.padding(0.0)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fixed(78.0))
|
||||||
.align_x(Alignment::Center),
|
.align_x(Alignment::Center),
|
||||||
)
|
);
|
||||||
|
} else {
|
||||||
|
// Empty transparent box for users without icons
|
||||||
|
column = column.push(
|
||||||
|
widget::container(widget::Space::new(
|
||||||
|
Length::Fixed(78.0),
|
||||||
|
Length::Fixed(78.0),
|
||||||
|
))
|
||||||
|
.padding(0.0)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fixed(78.0))
|
||||||
|
.align_x(Alignment::Center),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
column = column.push(
|
column = column.push(
|
||||||
widget::container(widget::text::title4(&user_data.full_name))
|
widget::container(widget::text::title4(&user_data.full_name))
|
||||||
|
|
@ -801,52 +836,59 @@ impl App {
|
||||||
if let Some((prompt, secret, value_opt)) = &self.common.prompt_opt {
|
if let Some((prompt, secret, value_opt)) = &self.common.prompt_opt {
|
||||||
match value_opt {
|
match value_opt {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
let text_input_id = self
|
// Only show password input when not authenticating
|
||||||
.common
|
if !self.authenticating {
|
||||||
.surface_names
|
let text_input_id = self
|
||||||
.get(&id)
|
.common
|
||||||
.and_then(|id| self.common.text_input_ids.get(id))
|
.surface_names
|
||||||
.cloned()
|
.get(&id)
|
||||||
.unwrap_or_else(|| cosmic::widget::Id::new("text_input"));
|
.and_then(|id| self.common.text_input_ids.get(id))
|
||||||
let mut text_input = widget::secure_input(
|
.cloned()
|
||||||
prompt.clone(),
|
.unwrap_or_else(|| cosmic::widget::Id::new("text_input"));
|
||||||
value.as_str(),
|
let mut text_input = widget::secure_input(
|
||||||
Some(
|
prompt.clone(),
|
||||||
|
value.as_str(),
|
||||||
|
Some(
|
||||||
|
common::Message::Prompt(
|
||||||
|
prompt.clone(),
|
||||||
|
!*secret,
|
||||||
|
Some(value.clone()),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
*secret,
|
||||||
|
)
|
||||||
|
.id(text_input_id)
|
||||||
|
.on_input(|input| {
|
||||||
common::Message::Prompt(
|
common::Message::Prompt(
|
||||||
prompt.clone(),
|
prompt.clone(),
|
||||||
!*secret,
|
*secret,
|
||||||
Some(value.clone()),
|
Some(input),
|
||||||
)
|
)
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
*secret,
|
|
||||||
)
|
|
||||||
.id(text_input_id)
|
|
||||||
.on_input(|input| {
|
|
||||||
common::Message::Prompt(prompt.clone(), *secret, Some(input))
|
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.on_submit(|v| Message::Auth(Some(v)));
|
.on_submit(|v| Message::Auth(Some(v)));
|
||||||
|
|
||||||
if let Some(text_input_id) = self
|
if let Some(text_input_id) = self
|
||||||
.common
|
.common
|
||||||
.surface_names
|
.surface_names
|
||||||
.get(&id)
|
.get(&id)
|
||||||
.and_then(|id| self.common.text_input_ids.get(id))
|
.and_then(|id| self.common.text_input_ids.get(id))
|
||||||
{
|
{
|
||||||
text_input = text_input.id(text_input_id.clone());
|
text_input = text_input.id(text_input_id.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if *secret {
|
if *secret {
|
||||||
text_input = text_input.password()
|
text_input = text_input.password()
|
||||||
}
|
}
|
||||||
|
|
||||||
column = column.push(text_input);
|
column = column.push(text_input);
|
||||||
|
|
||||||
if self.common.caps_lock {
|
if self.common.caps_lock {
|
||||||
column = column.push(widget::text(fl!("caps-lock")));
|
column = column.push(widget::text(fl!("caps-lock")));
|
||||||
} else if self.common.error_opt.is_none() {
|
} else if self.common.error_opt.is_none() {
|
||||||
column = column.push(widget::text(""));
|
column = column.push(widget::text(""));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -868,7 +910,27 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(error) = &self.common.error_opt {
|
// Show either authenticating message or error message in the same location
|
||||||
|
if self.authenticating {
|
||||||
|
column = column.push(
|
||||||
|
widget::container(
|
||||||
|
widget::row::with_capacity(2)
|
||||||
|
.spacing(8.0)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(
|
||||||
|
widget::icon::from_name("process-working-symbolic")
|
||||||
|
.size(16)
|
||||||
|
.icon()
|
||||||
|
.rotation(iced::Rotation::Floating(iced::Radians(
|
||||||
|
self.spinner_rotation.to_radians(),
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.push(widget::text(fl!("authenticating"))),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(Alignment::Center),
|
||||||
|
);
|
||||||
|
} else if let Some(error) = &self.common.error_opt {
|
||||||
column = column.push(
|
column = column.push(
|
||||||
widget::text(error)
|
widget::text(error)
|
||||||
.class(theme::Text::Color(iced::Color::from_rgb(1.0, 0.0, 0.0))),
|
.class(theme::Text::Color(iced::Color::from_rgb(1.0, 0.0, 0.0))),
|
||||||
|
|
@ -885,9 +947,8 @@ impl App {
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
};
|
};
|
||||||
let menu = widget::container(widget::column::with_children(vec![
|
let menu = widget::container(widget::column::with_children(vec![
|
||||||
widget::Space::with_height(Length::FillPortion(1)).into(),
|
|
||||||
widget::layer_container(
|
widget::layer_container(
|
||||||
iced::widget::row![left_element, right_element].align_y(Alignment::Center),
|
iced::widget::row![left_element, right_element].align_y(Alignment::Start),
|
||||||
)
|
)
|
||||||
.layer(cosmic::cosmic_theme::Layer::Background)
|
.layer(cosmic::cosmic_theme::Layer::Background)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
|
|
@ -905,7 +966,7 @@ impl App {
|
||||||
.class(cosmic::theme::Container::Background)
|
.class(cosmic::theme::Container::Background)
|
||||||
.width(Length::Fixed(800.0))
|
.width(Length::Fixed(800.0))
|
||||||
.into(),
|
.into(),
|
||||||
widget::Space::with_height(Length::FillPortion(4)).into(),
|
widget::Space::with_height(Length::Fill).into(),
|
||||||
]))
|
]))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
|
@ -1114,6 +1175,9 @@ impl cosmic::Application for App {
|
||||||
theme_builder: Default::default(),
|
theme_builder: Default::default(),
|
||||||
randr_list: None,
|
randr_list: None,
|
||||||
surface_id_pairs: Vec::new(),
|
surface_id_pairs: Vec::new(),
|
||||||
|
authenticating: false,
|
||||||
|
spinner_rotation: 0.0,
|
||||||
|
spinner_handle: None,
|
||||||
};
|
};
|
||||||
(app, Task::batch(tasks))
|
(app, Task::batch(tasks))
|
||||||
}
|
}
|
||||||
|
|
@ -1297,6 +1361,7 @@ impl cosmic::Application for App {
|
||||||
}
|
}
|
||||||
if self.entering_name || username != self.selected_username.username {
|
if self.entering_name || username != self.selected_username.username {
|
||||||
self.entering_name = false;
|
self.entering_name = false;
|
||||||
|
self.authenticating = false;
|
||||||
let data_idx = self
|
let data_idx = self
|
||||||
.flags
|
.flags
|
||||||
.user_datas
|
.user_datas
|
||||||
|
|
@ -1408,13 +1473,40 @@ impl cosmic::Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Auth(response) => {
|
Message::Auth(response) => {
|
||||||
self.common.prompt_opt = None;
|
|
||||||
self.common.error_opt = None;
|
self.common.error_opt = None;
|
||||||
|
self.authenticating = true;
|
||||||
self.send_request(Request::PostAuthMessageResponse { response });
|
self.send_request(Request::PostAuthMessageResponse { response });
|
||||||
|
|
||||||
|
// Start spinner animation if not already running
|
||||||
|
if self.spinner_handle.is_none() {
|
||||||
|
let (spinner_task, handle) = cosmic::task::stream(
|
||||||
|
cosmic::iced_futures::stream::channel(1, |mut msg_tx| async move {
|
||||||
|
let mut interval = time::interval(Duration::from_millis(16)); // ~60fps
|
||||||
|
loop {
|
||||||
|
msg_tx
|
||||||
|
.send(cosmic::Action::App(Message::SpinnerTick))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
interval.tick().await;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.abortable();
|
||||||
|
self.spinner_handle = Some(handle);
|
||||||
|
return spinner_task;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::Login => {
|
Message::Login => {
|
||||||
self.common.prompt_opt = None;
|
self.common.prompt_opt = None;
|
||||||
self.common.error_opt = None;
|
self.common.error_opt = None;
|
||||||
|
self.authenticating = false;
|
||||||
|
|
||||||
|
// Stop spinner animation
|
||||||
|
if let Some(handle) = self.spinner_handle.take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
self.spinner_rotation = 0.0;
|
||||||
|
|
||||||
match self.flags.sessions.get(&self.selected_session).cloned() {
|
match self.flags.sessions.get(&self.selected_session).cloned() {
|
||||||
Some((cmd, env)) => {
|
Some((cmd, env)) => {
|
||||||
self.send_request(Request::StartSession { cmd, env });
|
self.send_request(Request::StartSession { cmd, env });
|
||||||
|
|
@ -1425,6 +1517,14 @@ impl cosmic::Application for App {
|
||||||
}
|
}
|
||||||
Message::Error(error) => {
|
Message::Error(error) => {
|
||||||
self.common.error_opt = Some(error);
|
self.common.error_opt = Some(error);
|
||||||
|
self.authenticating = false;
|
||||||
|
|
||||||
|
// Stop spinner animation
|
||||||
|
if let Some(handle) = self.spinner_handle.take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
self.spinner_rotation = 0.0;
|
||||||
|
|
||||||
self.send_request(Request::CancelSession);
|
self.send_request(Request::CancelSession);
|
||||||
}
|
}
|
||||||
Message::Reconnect => {
|
Message::Reconnect => {
|
||||||
|
|
@ -1754,6 +1854,10 @@ impl cosmic::Application for App {
|
||||||
};
|
};
|
||||||
return reposition_subsurface(*subsurface_id, loc.x as i32, loc.y as i32);
|
return reposition_subsurface(*subsurface_id, loc.x as i32, loc.y as i32);
|
||||||
}
|
}
|
||||||
|
Message::SpinnerTick => {
|
||||||
|
// Update spinner rotation angle (360 degrees per second = 6 degrees per frame at 60fps)
|
||||||
|
self.spinner_rotation = (self.spinner_rotation + 6.0) % 360.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,31 @@ use std::time::Duration;
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::common;
|
use crate::{common, fl};
|
||||||
|
|
||||||
|
/// Convert greetd error descriptions to user-friendly localized messages
|
||||||
|
fn greetd_error_to_message(error_type: greetd_ipc::ErrorType, description: &str) -> String {
|
||||||
|
use greetd_ipc::ErrorType;
|
||||||
|
|
||||||
|
match error_type {
|
||||||
|
ErrorType::AuthError => {
|
||||||
|
// For authentication errors, check description for specific error types
|
||||||
|
if description.contains("PERM_DENIED") {
|
||||||
|
fl!("auth-error-denied")
|
||||||
|
} else if description.contains("MAXTRIES") {
|
||||||
|
fl!("auth-error-maxtries")
|
||||||
|
} else if description.contains("ACCT_EXPIRED") || description.contains("USER_UNKNOWN") {
|
||||||
|
fl!("auth-error-account")
|
||||||
|
} else {
|
||||||
|
fl!("auth-error-credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrorType::Error => {
|
||||||
|
// For generic errors, show a generic message
|
||||||
|
fl!("auth-error-default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn subscription() -> Subscription<Message> {
|
pub fn subscription() -> Subscription<Message> {
|
||||||
struct GreetdSubscription;
|
struct GreetdSubscription;
|
||||||
|
|
@ -89,10 +113,9 @@ pub fn subscription() -> Subscription<Message> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
greetd_ipc::Response::Error {
|
greetd_ipc::Response::Error {
|
||||||
error_type: _,
|
error_type,
|
||||||
description,
|
description,
|
||||||
} => {
|
} => {
|
||||||
//TODO: use error_type?
|
|
||||||
match request {
|
match request {
|
||||||
greetd_ipc::Request::CancelSession => {
|
greetd_ipc::Request::CancelSession => {
|
||||||
// Do not send errors for cancel session to gui
|
// Do not send errors for cancel session to gui
|
||||||
|
|
@ -105,7 +128,12 @@ pub fn subscription() -> Subscription<Message> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
_ = sender.send(Message::Error(description)).await;
|
_ = sender
|
||||||
|
.send(Message::Error(greetd_error_to_message(
|
||||||
|
error_type,
|
||||||
|
&description,
|
||||||
|
)))
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
174
src/locker.rs
174
src/locker.rs
|
|
@ -103,6 +103,31 @@ pub fn main(user: pwd::Passwd) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert PAM errors to user-friendly localized messages
|
||||||
|
fn pam_error_to_message(error: &pam_client::Error) -> String {
|
||||||
|
use pam_client::ErrorCode;
|
||||||
|
|
||||||
|
// Use the structured error code instead of string matching for reliability
|
||||||
|
match error.code() {
|
||||||
|
ErrorCode::AUTH_ERR | ErrorCode::CRED_INSUFFICIENT => {
|
||||||
|
fl!("auth-error-credentials")
|
||||||
|
}
|
||||||
|
ErrorCode::PERM_DENIED => {
|
||||||
|
fl!("auth-error-denied")
|
||||||
|
}
|
||||||
|
ErrorCode::MAXTRIES => {
|
||||||
|
fl!("auth-error-maxtries")
|
||||||
|
}
|
||||||
|
ErrorCode::ACCT_EXPIRED | ErrorCode::USER_UNKNOWN => {
|
||||||
|
fl!("auth-error-account")
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// For any other error, show a generic message
|
||||||
|
fl!("auth-error-default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pam_thread(username: String, conversation: Conversation) -> Result<(), pam_client::Error> {
|
pub fn pam_thread(username: String, conversation: Conversation) -> Result<(), pam_client::Error> {
|
||||||
//TODO: send errors to GUI, restart process
|
//TODO: send errors to GUI, restart process
|
||||||
|
|
||||||
|
|
@ -241,6 +266,7 @@ pub enum Message {
|
||||||
Error(String),
|
Error(String),
|
||||||
Lock,
|
Lock,
|
||||||
Unlock,
|
Unlock,
|
||||||
|
SpinnerTick,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<common::Message> for Message {
|
impl From<common::Message> for Message {
|
||||||
|
|
@ -277,6 +303,9 @@ pub struct App {
|
||||||
dropdown_opt: Option<Dropdown>,
|
dropdown_opt: Option<Dropdown>,
|
||||||
inhibit_opt: Option<Arc<OwnedFd>>,
|
inhibit_opt: Option<Arc<OwnedFd>>,
|
||||||
value_tx_opt: Option<mpsc::Sender<String>>,
|
value_tx_opt: Option<mpsc::Sender<String>>,
|
||||||
|
authenticating: bool,
|
||||||
|
spinner_rotation: f32,
|
||||||
|
spinner_handle: Option<cosmic::iced::task::Handle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
|
@ -422,16 +451,39 @@ impl App {
|
||||||
.spacing(12.0)
|
.spacing(12.0)
|
||||||
.max_width(280.0);
|
.max_width(280.0);
|
||||||
|
|
||||||
if let Some(icon) = &self.flags.user_icon {
|
let military_time = self.flags.user_data.time_applet_config.military_time;
|
||||||
|
let space_height = match military_time {
|
||||||
|
true => 63.0,
|
||||||
|
false => 10.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add top spacing for better visual appearance
|
||||||
|
// Bottom of the password text input field should align with bottom of time widget
|
||||||
|
column = column.push(widget::Space::with_height(Length::Fixed(space_height)));
|
||||||
|
|
||||||
|
// Display user icon or empty transparent box
|
||||||
|
if let Some(icon_handle) = &self.flags.user_icon {
|
||||||
column = column.push(
|
column = column.push(
|
||||||
widget::container(
|
widget::container(
|
||||||
widget::image(icon)
|
widget::image(icon_handle)
|
||||||
.width(Length::Fixed(78.0))
|
.width(Length::Fixed(78.0))
|
||||||
.height(Length::Fixed(78.0)),
|
.height(Length::Fixed(78.0))
|
||||||
|
.content_fit(iced::ContentFit::Fill),
|
||||||
)
|
)
|
||||||
|
.padding(0.0)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fixed(78.0))
|
||||||
.align_x(Alignment::Center),
|
.align_x(Alignment::Center),
|
||||||
)
|
);
|
||||||
|
} else {
|
||||||
|
// Empty transparent box for users without icons
|
||||||
|
column = column.push(
|
||||||
|
widget::container(widget::Space::new(Length::Fixed(78.0), Length::Fixed(78.0)))
|
||||||
|
.padding(0.0)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fixed(78.0))
|
||||||
|
.align_x(Alignment::Center),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
column = column.push(
|
column = column.push(
|
||||||
|
|
@ -464,11 +516,17 @@ impl App {
|
||||||
),
|
),
|
||||||
*secret,
|
*secret,
|
||||||
)
|
)
|
||||||
.id(text_input_id)
|
.id(text_input_id);
|
||||||
.on_input(|input| {
|
|
||||||
common::Message::Prompt(prompt.clone(), *secret, Some(input)).into()
|
// Don't allow input when authenticating
|
||||||
})
|
if !self.authenticating {
|
||||||
.on_submit(Message::Submit);
|
text_input = text_input
|
||||||
|
.on_input(|input| {
|
||||||
|
common::Message::Prompt(prompt.clone(), *secret, Some(input))
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.on_submit(Message::Submit);
|
||||||
|
}
|
||||||
|
|
||||||
if *secret {
|
if *secret {
|
||||||
text_input = text_input.password()
|
text_input = text_input.password()
|
||||||
|
|
@ -476,7 +534,7 @@ impl App {
|
||||||
|
|
||||||
column = column.push(text_input);
|
column = column.push(text_input);
|
||||||
|
|
||||||
if self.common.caps_lock {
|
if self.common.caps_lock && !self.authenticating {
|
||||||
column = column.push(widget::text(fl!("caps-lock")));
|
column = column.push(widget::text(fl!("caps-lock")));
|
||||||
} else if self.common.error_opt.is_none() {
|
} else if self.common.error_opt.is_none() {
|
||||||
column = column.push(widget::text(""));
|
column = column.push(widget::text(""));
|
||||||
|
|
@ -488,7 +546,27 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(error) = &self.common.error_opt {
|
// Show either authenticating message or error message in the same location
|
||||||
|
if self.authenticating {
|
||||||
|
column = column.push(
|
||||||
|
widget::container(
|
||||||
|
widget::row::with_capacity(2)
|
||||||
|
.spacing(8.0)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(
|
||||||
|
widget::icon::from_name("process-working-symbolic")
|
||||||
|
.size(16)
|
||||||
|
.icon()
|
||||||
|
.rotation(iced::Rotation::Floating(iced::Radians(
|
||||||
|
self.spinner_rotation.to_radians(),
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.push(widget::text(fl!("authenticating"))),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(Alignment::Center),
|
||||||
|
);
|
||||||
|
} else if let Some(error) = &self.common.error_opt {
|
||||||
column = column.push(
|
column = column.push(
|
||||||
widget::text(error)
|
widget::text(error)
|
||||||
.class(theme::Text::Color(iced::Color::from_rgb(1.0, 0.0, 0.0))),
|
.class(theme::Text::Color(iced::Color::from_rgb(1.0, 0.0, 0.0))),
|
||||||
|
|
@ -506,9 +584,8 @@ impl App {
|
||||||
};
|
};
|
||||||
|
|
||||||
widget::container(widget::column::with_children(vec![
|
widget::container(widget::column::with_children(vec![
|
||||||
widget::Space::with_height(Length::FillPortion(1)).into(),
|
|
||||||
widget::layer_container(
|
widget::layer_container(
|
||||||
iced::widget::row![left_element, right_element].align_y(Alignment::Center),
|
iced::widget::row![left_element, right_element].align_y(Alignment::Start),
|
||||||
)
|
)
|
||||||
.layer(cosmic::cosmic_theme::Layer::Background)
|
.layer(cosmic::cosmic_theme::Layer::Background)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
|
|
@ -526,7 +603,7 @@ impl App {
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Shrink)
|
.height(Length::Shrink)
|
||||||
.into(),
|
.into(),
|
||||||
widget::Space::with_height(Length::FillPortion(4)).into(),
|
widget::Space::with_height(Length::Fill).into(),
|
||||||
]))
|
]))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
|
@ -579,6 +656,9 @@ impl cosmic::Application for App {
|
||||||
dropdown_opt: None,
|
dropdown_opt: None,
|
||||||
inhibit_opt: None,
|
inhibit_opt: None,
|
||||||
value_tx_opt: None,
|
value_tx_opt: None,
|
||||||
|
authenticating: false,
|
||||||
|
spinner_rotation: 0.0,
|
||||||
|
spinner_handle: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let task = if cfg!(feature = "logind") {
|
let task = if cfg!(feature = "logind") {
|
||||||
|
|
@ -817,7 +897,7 @@ impl cosmic::Application for App {
|
||||||
tracing::warn!("authentication error: {}", err);
|
tracing::warn!("authentication error: {}", err);
|
||||||
msg_tx
|
msg_tx
|
||||||
.send(cosmic::Action::App(Message::Error(
|
.send(cosmic::Action::App(Message::Error(
|
||||||
err.to_string(),
|
pam_error_to_message(&err),
|
||||||
)))
|
)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -941,16 +1021,41 @@ impl cosmic::Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Submit(value) => {
|
Message::Submit(value) => {
|
||||||
self.common.prompt_opt = None;
|
|
||||||
self.common.error_opt = None;
|
self.common.error_opt = None;
|
||||||
|
self.authenticating = true;
|
||||||
match self.value_tx_opt.take() {
|
match self.value_tx_opt.take() {
|
||||||
Some(value_tx) => {
|
Some(value_tx) => {
|
||||||
// Clear errors
|
// Start spinner animation if not already running
|
||||||
self.common.error_opt = None;
|
if self.spinner_handle.is_none() {
|
||||||
return cosmic::task::future(async move {
|
let (spinner_task, handle) = cosmic::task::stream(
|
||||||
value_tx.send(value).await.unwrap();
|
cosmic::iced_futures::stream::channel(1, |mut msg_tx| async move {
|
||||||
Message::Channel(value_tx)
|
let mut interval =
|
||||||
});
|
tokio::time::interval(Duration::from_millis(16)); // ~60fps
|
||||||
|
loop {
|
||||||
|
msg_tx
|
||||||
|
.send(cosmic::Action::App(Message::SpinnerTick))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
interval.tick().await;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.abortable();
|
||||||
|
self.spinner_handle = Some(handle);
|
||||||
|
|
||||||
|
return Task::batch([
|
||||||
|
spinner_task,
|
||||||
|
cosmic::task::future(async move {
|
||||||
|
value_tx.send(value).await.unwrap();
|
||||||
|
Message::Channel(value_tx)
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return cosmic::task::future(async move {
|
||||||
|
value_tx.send(value).await.unwrap();
|
||||||
|
Message::Channel(value_tx)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => tracing::warn!("tried to submit when value_tx_opt not set"),
|
None => tracing::warn!("tried to submit when value_tx_opt not set"),
|
||||||
}
|
}
|
||||||
|
|
@ -968,6 +1073,17 @@ impl cosmic::Application for App {
|
||||||
}
|
}
|
||||||
Message::Error(error) => {
|
Message::Error(error) => {
|
||||||
self.common.error_opt = Some(error);
|
self.common.error_opt = Some(error);
|
||||||
|
self.authenticating = false;
|
||||||
|
|
||||||
|
// Stop spinner animation
|
||||||
|
if let Some(handle) = self.spinner_handle.take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
self.spinner_rotation = 0.0;
|
||||||
|
}
|
||||||
|
Message::SpinnerTick => {
|
||||||
|
// Update spinner rotation angle (360 degrees per second = 6 degrees per frame at 60fps)
|
||||||
|
self.spinner_rotation = (self.spinner_rotation + 6.0) % 360.0;
|
||||||
}
|
}
|
||||||
Message::Lock => match self.state {
|
Message::Lock => match self.state {
|
||||||
State::Unlocked => {
|
State::Unlocked => {
|
||||||
|
|
@ -977,6 +1093,12 @@ impl cosmic::Application for App {
|
||||||
self.common.error_opt = None;
|
self.common.error_opt = None;
|
||||||
// Clear value_tx
|
// Clear value_tx
|
||||||
self.value_tx_opt = None;
|
self.value_tx_opt = None;
|
||||||
|
// Reset authenticating state
|
||||||
|
self.authenticating = false;
|
||||||
|
if let Some(handle) = self.spinner_handle.take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
self.spinner_rotation = 0.0;
|
||||||
// Try to create lockfile when locking
|
// Try to create lockfile when locking
|
||||||
if let Some(ref lockfile) = self.flags.lockfile_opt {
|
if let Some(ref lockfile) = self.flags.lockfile_opt {
|
||||||
if let Err(err) = fs::File::create(lockfile) {
|
if let Err(err) = fs::File::create(lockfile) {
|
||||||
|
|
@ -1002,6 +1124,14 @@ impl cosmic::Application for App {
|
||||||
self.common.error_opt = None;
|
self.common.error_opt = None;
|
||||||
// Clear value_tx
|
// Clear value_tx
|
||||||
self.value_tx_opt = None;
|
self.value_tx_opt = None;
|
||||||
|
// Stop authenticating
|
||||||
|
self.authenticating = false;
|
||||||
|
|
||||||
|
// Stop spinner animation
|
||||||
|
if let Some(handle) = self.spinner_handle.take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
self.spinner_rotation = 0.0;
|
||||||
// Try to delete lockfile when unlocking
|
// Try to delete lockfile when unlocking
|
||||||
if let Some(ref lockfile) = self.flags.lockfile_opt {
|
if let Some(ref lockfile) = self.flags.lockfile_opt {
|
||||||
if let Err(err) = fs::remove_file(lockfile) {
|
if let Err(err) = fs::remove_file(lockfile) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue