use std::cmp::Ordering;
use gettextrs::gettext;
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use indexmap::map::IndexMap;
use tracing::{error, info};
mod failed_session;
mod new_session;
mod session_info;
mod session_list_settings;
pub use self::{failed_session::*, new_session::*, session_info::*, session_list_settings::*};
use crate::{
prelude::*,
secret::{self, StoredSession},
session::model::{Session, SessionState},
spawn, spawn_tokio,
utils::LoadingState,
};
mod imp {
use std::{
cell::{Cell, RefCell},
marker::PhantomData,
};
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::SessionList)]
pub struct SessionList {
#[property(get, builder(LoadingState::default()))]
pub state: Cell<LoadingState>,
#[property(get, nullable)]
pub error: RefCell<Option<String>>,
pub list: RefCell<IndexMap<String, SessionInfo>>,
pub settings: SessionListSettings,
#[property(get = Self::is_empty)]
pub is_empty: PhantomData<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for SessionList {
const NAME: &'static str = "SessionList";
type Type = super::SessionList;
type Interfaces = (gio::ListModel,);
}
#[glib::derived_properties]
impl ObjectImpl for SessionList {}
impl ListModelImpl for SessionList {
fn item_type(&self) -> glib::Type {
SessionInfo::static_type()
}
fn n_items(&self) -> u32 {
self.list.borrow().len() as u32
}
fn item(&self, position: u32) -> Option<glib::Object> {
self.list
.borrow()
.get_index(position as usize)
.map(|(_, v)| v.upcast_ref::<glib::Object>())
.cloned()
}
}
impl SessionList {
fn is_empty(&self) -> bool {
self.list.borrow().is_empty()
}
}
}
glib::wrapper! {
pub struct SessionList(ObjectSubclass<imp::SessionList>)
@implements gio::ListModel;
}
impl SessionList {
pub fn new() -> Self {
glib::Object::new()
}
fn set_state(&self, state: LoadingState) {
if self.state() == state {
return;
}
self.imp().state.set(state);
self.notify_state();
}
fn set_error(&self, message: String) {
self.imp().error.replace(Some(message));
self.notify_error();
}
pub fn settings(&self) -> &SessionListSettings {
&self.imp().settings
}
pub fn has_new_sessions(&self) -> bool {
self.imp()
.list
.borrow()
.values()
.any(|s| s.is::<NewSession>())
}
pub fn has_session_ready(&self) -> bool {
self.imp()
.list
.borrow()
.values()
.filter_map(|s| s.downcast_ref::<Session>())
.any(|s| s.state() == SessionState::Ready)
}
pub fn get(&self, session_id: &str) -> Option<SessionInfo> {
self.imp().list.borrow().get(session_id).cloned()
}
pub fn index(&self, session_id: &str) -> Option<usize> {
self.imp().list.borrow().get_index_of(session_id)
}
pub fn first(&self) -> Option<SessionInfo> {
self.imp().list.borrow().first().map(|(_, v)| v.clone())
}
pub fn insert(&self, session: impl IsA<SessionInfo>) -> usize {
let session = session.upcast();
if let Some(session) = session.downcast_ref::<Session>() {
if session.state() == SessionState::Ready {
spawn!(clone!(
#[weak]
session,
async move { session.init_notifications().await }
));
} else {
session.connect_ready(|session| {
spawn!(clone!(
#[weak]
session,
async move { session.init_notifications().await }
));
});
}
session.connect_logged_out(clone!(
#[weak(rename_to = obj)]
self,
move |session| obj.remove(session.session_id())
));
}
let was_empty = self.is_empty();
let (index, replaced) = self
.imp()
.list
.borrow_mut()
.insert_full(session.session_id().to_owned(), session);
let removed = if replaced.is_some() { 1 } else { 0 };
self.items_changed(index as u32, removed, 1);
if was_empty {
self.notify_is_empty();
}
index
}
pub fn remove(&self, session_id: &str) {
let removed = self.imp().list.borrow_mut().shift_remove_full(session_id);
if let Some((position, ..)) = removed {
self.items_changed(position as u32, 1, 0);
if self.is_empty() {
self.notify_is_empty();
}
}
}
pub async fn restore_sessions(&self) {
if self.state() >= LoadingState::Loading {
return;
}
self.set_state(LoadingState::Loading);
let handle = spawn_tokio!(secret::restore_sessions());
match handle.await.unwrap() {
Ok(mut sessions) => {
let settings = self.settings();
settings.load();
let session_ids = settings.session_ids();
sessions.sort_by(|a, b| {
let pos_a = session_ids.get_index_of(&a.id);
let pos_b = session_ids.get_index_of(&b.id);
match (pos_a, pos_b) {
(Some(pos_a), Some(pos_b)) => pos_a.cmp(&pos_b),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
_ => Ordering::Equal,
}
});
for stored_session in sessions {
info!(
"Restoring previous session {} for user {}",
stored_session.id, stored_session.user_id,
);
self.insert(NewSession::new(stored_session.clone()));
spawn!(
glib::Priority::DEFAULT_IDLE,
clone!(
#[weak(rename_to = obj)]
self,
async move {
obj.restore_stored_session(stored_session).await;
}
)
);
}
self.set_state(LoadingState::Ready)
}
Err(error) => {
let message = format!(
"{}\n\n{}",
gettext("Could not restore previous sessions"),
error.to_user_facing(),
);
self.set_error(message);
self.set_state(LoadingState::Error);
}
}
}
async fn restore_stored_session(&self, session_info: StoredSession) {
let settings = self.settings().get_or_create(&session_info.id);
match Session::restore(session_info.clone(), settings).await {
Ok(session) => {
session.prepare().await;
self.insert(session);
}
Err(error) => {
error!("Could not restore previous session: {error}");
self.insert(FailedSession::new(session_info, error));
}
}
}
}
impl Default for SessionList {
fn default() -> Self {
Self::new()
}
}