320 lines
11 KiB
Rust
320 lines
11 KiB
Rust
use adw::{self, prelude::*};
|
|
use gettextrs::gettext;
|
|
use gtk::{glib, glib::clone, subclass::prelude::*, CompositeTemplate};
|
|
use log::error;
|
|
|
|
use super::Device;
|
|
use crate::{
|
|
components::{AuthError, SpinnerButton},
|
|
gettext_f, spawn, toast,
|
|
};
|
|
|
|
mod imp {
|
|
use std::cell::{Cell, RefCell};
|
|
|
|
use glib::subclass::InitializingObject;
|
|
use once_cell::sync::Lazy;
|
|
|
|
use super::*;
|
|
|
|
#[derive(Debug, Default, CompositeTemplate)]
|
|
#[template(
|
|
resource = "/org/gnome/Fractal/ui/session/view/account_settings/devices_page/device_row.ui"
|
|
)]
|
|
pub struct DeviceRow {
|
|
#[template_child]
|
|
pub display_name: TemplateChild<gtk::Label>,
|
|
#[template_child]
|
|
pub verified_icon: TemplateChild<gtk::Image>,
|
|
#[template_child]
|
|
pub last_seen_ip: TemplateChild<gtk::Label>,
|
|
#[template_child]
|
|
pub last_seen_ts: TemplateChild<gtk::Label>,
|
|
#[template_child]
|
|
pub delete_logout_button: TemplateChild<SpinnerButton>,
|
|
#[template_child]
|
|
pub verify_button: TemplateChild<SpinnerButton>,
|
|
pub device: RefCell<Option<Device>>,
|
|
pub is_current_device: Cell<bool>,
|
|
}
|
|
|
|
#[glib::object_subclass]
|
|
impl ObjectSubclass for DeviceRow {
|
|
const NAME: &'static str = "AccountSettingsDeviceRow";
|
|
type Type = super::DeviceRow;
|
|
type ParentType = gtk::ListBoxRow;
|
|
|
|
fn class_init(klass: &mut Self::Class) {
|
|
Self::bind_template(klass);
|
|
}
|
|
|
|
fn instance_init(obj: &InitializingObject<Self>) {
|
|
obj.init_template();
|
|
}
|
|
}
|
|
|
|
impl ObjectImpl for DeviceRow {
|
|
fn properties() -> &'static [glib::ParamSpec] {
|
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
|
vec![
|
|
glib::ParamSpecObject::builder::<Device>("device")
|
|
.construct_only()
|
|
.build(),
|
|
glib::ParamSpecBoolean::builder("is-current-device")
|
|
.construct_only()
|
|
.build(),
|
|
]
|
|
});
|
|
|
|
PROPERTIES.as_ref()
|
|
}
|
|
|
|
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
|
let obj = self.obj();
|
|
|
|
match pspec.name() {
|
|
"device" => {
|
|
obj.set_device(value.get().unwrap());
|
|
}
|
|
"is-current-device" => {
|
|
obj.set_current_device(value.get().unwrap());
|
|
}
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
|
|
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
|
let obj = self.obj();
|
|
|
|
match pspec.name() {
|
|
"device" => obj.device().to_value(),
|
|
"is-current-device" => obj.is_current_device().to_value(),
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
|
|
fn constructed(&self) {
|
|
self.parent_constructed();
|
|
let obj = self.obj();
|
|
|
|
match &obj.is_current_device() {
|
|
false => self
|
|
.delete_logout_button
|
|
.set_label(&gettext("Disconnect Session")),
|
|
true => {
|
|
self.delete_logout_button.set_label(&gettext("Log Out"));
|
|
self.delete_logout_button
|
|
.add_css_class("destructive-action");
|
|
}
|
|
}
|
|
|
|
self.delete_logout_button
|
|
.connect_clicked(clone!(@weak obj => move |_| {
|
|
match &obj.is_current_device() {
|
|
false=> obj.delete(),
|
|
true => obj.activate_action("account-settings.logout", None).unwrap()
|
|
}
|
|
}));
|
|
|
|
self.verify_button
|
|
.connect_clicked(clone!(@weak obj => move |_| {
|
|
todo!("Not implemented");
|
|
}));
|
|
}
|
|
}
|
|
impl WidgetImpl for DeviceRow {}
|
|
impl ListBoxRowImpl for DeviceRow {}
|
|
}
|
|
|
|
glib::wrapper! {
|
|
pub struct DeviceRow(ObjectSubclass<imp::DeviceRow>)
|
|
@extends gtk::Widget, gtk::ListBoxRow, @implements gtk::Accessible;
|
|
}
|
|
|
|
impl DeviceRow {
|
|
pub fn new(device: &Device, is_current_device: bool) -> Self {
|
|
glib::Object::builder()
|
|
.property("device", device)
|
|
.property("is-current-device", is_current_device)
|
|
.build()
|
|
}
|
|
|
|
/// The device displayed by this row.
|
|
pub fn device(&self) -> Option<Device> {
|
|
self.imp().device.borrow().clone()
|
|
}
|
|
|
|
/// Set the device displayed by this row.
|
|
pub fn set_device(&self, device: Option<Device>) {
|
|
let imp = self.imp();
|
|
|
|
if self.device() == device {
|
|
return;
|
|
}
|
|
|
|
if let Some(ref device) = device {
|
|
imp.display_name.set_label(device.display_name());
|
|
self.set_tooltip_text(Some(device.device_id().as_str()));
|
|
|
|
imp.verified_icon.set_visible(device.is_verified());
|
|
// TODO: Implement verification
|
|
// imp.verify_button.set_visible(!device.is_verified());
|
|
|
|
let last_seen_ip_visible = if let Some(last_seen_ip) = device.last_seen_ip() {
|
|
imp.last_seen_ip.set_label(last_seen_ip);
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
imp.last_seen_ip.set_visible(last_seen_ip_visible);
|
|
|
|
let last_seen_ts_visible = if let Some(last_seen_ts) = device.last_seen_ts() {
|
|
let last_seen_ts = format_date_time_as_string(last_seen_ts);
|
|
imp.last_seen_ts.set_label(&last_seen_ts);
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
imp.last_seen_ts.set_visible(last_seen_ts_visible);
|
|
}
|
|
|
|
imp.device.replace(device);
|
|
self.notify("device");
|
|
}
|
|
|
|
/// Set whether this is the device of the current session.
|
|
fn set_current_device(&self, input_bool: bool) {
|
|
let imp = self.imp();
|
|
if imp.is_current_device.get() == input_bool {
|
|
return;
|
|
}
|
|
imp.is_current_device.replace(input_bool);
|
|
self.notify("is-current-device");
|
|
}
|
|
|
|
/// Whether this is the device of the current session.
|
|
pub fn is_current_device(&self) -> bool {
|
|
self.imp().is_current_device.get()
|
|
}
|
|
|
|
fn delete(&self) {
|
|
self.imp().delete_logout_button.set_loading(true);
|
|
|
|
let Some(device) = self.device() else {
|
|
return;
|
|
};
|
|
|
|
spawn!(clone!(@weak self as obj => async move {
|
|
let window: Option<gtk::Window> = obj.root().and_then(|root| root.downcast().ok());
|
|
match device.delete(window.as_ref()).await {
|
|
Ok(_) => obj.set_visible(false),
|
|
Err(AuthError::UserCancelled) => {},
|
|
Err(error) => {
|
|
error!("Failed to disconnect device {}: {error:?}", device.device_id());
|
|
let device_name = device.display_name();
|
|
// Translators: Do NOT translate the content between '{' and '}', this is a variable name.
|
|
let error_message = gettext_f("Failed to disconnect device “{device_name}”", &[("device_name", device_name)]);
|
|
toast!(obj, error_message);
|
|
},
|
|
}
|
|
obj.imp().delete_logout_button.set_loading(false);
|
|
}));
|
|
}
|
|
}
|
|
|
|
// This was ported from Nautilus and simplified for our use case.
|
|
// See: https://gitlab.gnome.org/GNOME/nautilus/-/blob/master/src/nautilus-file.c#L5488
|
|
pub fn format_date_time_as_string(datetime: glib::DateTime) -> glib::GString {
|
|
let now = glib::DateTime::now_local().unwrap();
|
|
let format;
|
|
let days_ago = {
|
|
let today_midnight =
|
|
glib::DateTime::from_local(now.year(), now.month(), now.day_of_month(), 0, 0, 0f64)
|
|
.unwrap();
|
|
|
|
let date = glib::DateTime::from_local(
|
|
datetime.year(),
|
|
datetime.month(),
|
|
datetime.day_of_month(),
|
|
0,
|
|
0,
|
|
0f64,
|
|
)
|
|
.unwrap();
|
|
|
|
today_midnight.difference(&date).as_days()
|
|
};
|
|
|
|
let use_24 = {
|
|
let local_time = datetime.format("%X").unwrap().as_str().to_ascii_lowercase();
|
|
local_time.ends_with("am") || local_time.ends_with("pm")
|
|
};
|
|
|
|
// Show only the time if date is on today
|
|
if days_ago == 0 {
|
|
if use_24 {
|
|
// Translators: Time in 24h format
|
|
format = gettext("Last seen at %H:%M");
|
|
} else {
|
|
// Translators: Time in 12h format
|
|
format = gettext("Last seen at %l:%M %p");
|
|
}
|
|
}
|
|
// Show the word "Yesterday" and time if date is on yesterday
|
|
else if days_ago == 1 {
|
|
if use_24 {
|
|
// Translators: this is the word Yesterday followed by
|
|
// a time in 24h format. i.e. "Last seen Yesterday at 23:04"
|
|
// xgettext:no-c-format
|
|
format = gettext("Last seen Yesterday at %H:%M");
|
|
} else {
|
|
// Translators: this is the word Yesterday followed by
|
|
// a time in 12h format. i.e. "Last seen Yesterday at 9:04 PM"
|
|
// xgettext:no-c-format
|
|
format = gettext("Last seen Yesterday at %l:%M %p");
|
|
}
|
|
}
|
|
// Show a week day and time if date is in the last week
|
|
else if days_ago > 1 && days_ago < 7 {
|
|
if use_24 {
|
|
// Translators: this is the name of the week day followed by
|
|
// a time in 24h format. i.e. "Last seen Monday at 23:04"
|
|
// xgettext:no-c-format
|
|
format = gettext("Last seen %A at %H:%M");
|
|
} else {
|
|
// Translators: this is the week day name followed by
|
|
// a time in 12h format. i.e. "Last seen Monday at 9:04 PM"
|
|
// xgettext:no-c-format
|
|
format = gettext("Last seen %A at %l:%M %p");
|
|
}
|
|
} else if datetime.year() == now.year() {
|
|
if use_24 {
|
|
// Translators: this is the day of the month followed
|
|
// by the abbreviated month name followed by a time in
|
|
// 24h format i.e. "Last seen February 3 at 23:04"
|
|
// xgettext:no-c-format
|
|
format = gettext("Last seen %B %-e at %H:%M");
|
|
} else {
|
|
// Translators: this is the day of the month followed
|
|
// by the abbreviated month name followed by a time in
|
|
// 12h format i.e. "Last seen February 3 at 9:04 PM"
|
|
// xgettext:no-c-format
|
|
format = gettext("Last seen %B %-e at %l:%M %p");
|
|
}
|
|
} else if use_24 {
|
|
// Translators: this is the day number followed
|
|
// by the abbreviated month name followed by the year followed
|
|
// by a time in 24h format i.e. "Last seen February 3 2015 at 23:04"
|
|
// xgettext:no-c-format
|
|
format = gettext("Last seen %B %-e %Y at %H:%M");
|
|
} else {
|
|
// Translators: this is the day number followed
|
|
// by the abbreviated month name followed by the year followed
|
|
// by a time in 12h format i.e. "Last seen February 3 2015 at 9:04 PM"
|
|
// xgettext:no-c-format
|
|
format = gettext("Last seen %B %-e %Y at %l:%M %p");
|
|
}
|
|
|
|
datetime.format(&format).unwrap()
|
|
}
|