[Add] Terms UI, [WIP] Main UI

This commit is contained in:
Alex Emmet 2026-02-18 19:11:43 +01:00
commit bac1c608fe
13 changed files with 450 additions and 112 deletions

View file

@ -1,5 +1,5 @@
use crate::ACTIVE_TASKS;
use crate::gui::ui::UI;
use crate::gui::ui::{UI, UNIQUE};
use crate::{RELOAD, SHUTDOWN};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
use std::sync::Arc;
@ -31,8 +31,9 @@ pub fn setup_input_handler(ui: Arc<UI>) {
Ok(event) => {
if let Event::Key(key_event) = event {
if key_event.kind == KeyEventKind::Press {
let ui_clone = ui.clone();
handle_input(key_event, ui_clone).await;
let uic = ui.clone();
handle_input(key_event, uic).await;
*UNIQUE.write().await = true;
}
}
}

View file

@ -1,9 +1,10 @@
use std::fmt::Debug;
use std::fmt::{Debug, Formatter};
use std::future::Future;
use std::pin::Pin;
use crate::gui::screens::screens::Screen;
#[allow(unused)]
pub enum InteractionResult {
CloseScreen,
OpenScreen {
@ -17,7 +18,7 @@ pub enum InteractionResult {
}
impl Debug for InteractionResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"),
InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"),

View file

@ -3,6 +3,7 @@ pub mod elements {
pub mod log_card;
}
pub mod screens {
pub mod main_screen;
pub mod screens;
pub mod terms_checker;
pub mod terms_updater;

View file

@ -0,0 +1,41 @@
use std::{any::Any, sync::Arc};
use crossterm::event::KeyEvent;
use ratatui::{
Frame,
layout::Rect,
widgets::{Block, Borders},
};
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen, ui::UI};
pub struct MainScreen {
ui: Arc<UI>,
}
impl MainScreen {
pub async fn new(ui: Arc<UI>) -> Self {
MainScreen { ui }
}
}
impl Screen for MainScreen {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_ui(&self) -> &Arc<UI> {
&self.ui
}
fn render(&self, f: &mut Frame, rect: Rect) {
f.render_widget(Block::default().title("Main").borders(Borders::ALL), rect);
}
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
InteractionResult::Unhandled
}
}

View file

@ -1,15 +1,47 @@
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen};
use crate::{
SHUTDOWN,
gui::{
input_handler::setup_input_handler, interaction_result::InteractionResult,
screens::screens::Screen,
},
};
use crossterm::event::KeyEvent;
use once_cell::sync::Lazy;
use ratatui::{Terminal, backend::CrosstermBackend, init};
use std::sync::{Arc, Mutex};
use std::{
io::Stdout,
sync::{Arc, Mutex},
time::Duration,
};
use tokio::sync::RwLock;
/// UI state and rendering
pub static UNIQUE: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true));
pub struct UI {
pub terminal: Arc<Mutex<Terminal<CrosstermBackend<std::io::Stdout>>>>,
pub terminal: Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>,
screen: Arc<RwLock<Option<Box<dyn Screen>>>>,
}
pub fn start_tui() -> Arc<UI> {
let ui = Arc::new(UI::new());
let uic = ui.clone();
tokio::spawn(async move {
loop {
if *SHUTDOWN.read().await {
break;
}
if *UNIQUE.read().await {
uic.render().await;
} else {
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
ratatui::restore();
});
setup_input_handler(ui.clone());
ui
}
impl UI {
pub fn new() -> Self {
let terminal = init();
@ -42,9 +74,7 @@ impl UI {
ui.set_screen(screen).await;
}
InteractionResult::CloseScreen => {
/* TODO
self.set_screen();
*/
*self.screen.write().await = None;
}
InteractionResult::Handled => {}
InteractionResult::Unhandled => {}

View file

@ -20,8 +20,8 @@ mod util;
use crate::communities::community_manager;
use crate::communities::interactables::registry;
use crate::gui::app_state::AppState;
use crate::gui::input_handler::setup_input_handler;
use crate::gui::ui::UI;
use crate::gui::screens::main_screen::MainScreen;
use crate::gui::ui::start_tui;
use crate::langu::language_creator;
use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection};
use crate::server::server::start;
@ -46,8 +46,7 @@ async fn main() {
*RELOAD.write().await = false;
*SHUTDOWN.write().await = false;
let ui = Arc::new(UI::new());
setup_input_handler(ui.clone());
let ui = start_tui();
let ui_clone = ui.clone();
tokio::spawn(async move {
@ -60,15 +59,25 @@ async fn main() {
let (eula, tos_pp) = ConsentManager::check(ui.clone()).await;
if !eula {
ui.terminal.lock().unwrap().clear();
ui.terminal.lock().unwrap().flush();
*SHUTDOWN.write().await = true;
loop {
if ACTIVE_TASKS.lock().unwrap().is_empty() {
break;
}
sleep(Duration::from_secs(1)).await;
}
println!("You need to accept our End User Licence Agreement before launching!");
println!("You can find this at 'agreements'!");
return;
}
if !tos_pp {
ui.terminal.lock().unwrap().clear();
ui.terminal.lock().unwrap().flush();
*SHUTDOWN.write().await = true;
loop {
if ACTIVE_TASKS.lock().unwrap().is_empty() {
break;
}
sleep(Duration::from_secs(1)).await;
}
println!(
"Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!"
);
@ -77,6 +86,9 @@ async fn main() {
return;
}
let main_screen = MainScreen::new(ui.clone()).await;
ui.set_screen(Box::new(main_screen)).await;
// LANGUAGE PACK
if let Err(e) = language_creator::create_languages() {
println!("Language pack creation failed: {}", e);

View file

@ -17,125 +17,99 @@ use std::time::{SystemTime, UNIX_EPOCH};
pub struct ConsentManager;
impl ConsentManager {
pub async fn check(ui: Arc<UI>) -> (bool, bool) {
let (tx, rx) = oneshot::channel();
let file = load_file("", "agreements");
let mut file_state = ConsentState::from_str(&file).sanitize();
ui.set_screen(Box::new(TermsCheckerScreen::new(ui.clone(), Some(tx))))
.await;
let mut state = ConsentState::from_str(&file).sanitize();
let result = rx.await.unwrap_or(UserChoice::Deny);
if !state.accepted_eula {
let (tx, rx) = oneshot::channel();
match result {
UserChoice::AcceptEULA | UserChoice::AcceptAll => {
if let Some((current_eula, current_tos, current_privacy)) = get_current_docs().await
{
file_state.accepted_eula = true;
file_state.eula = Some(current_eula);
ui.set_screen(Box::new(TermsCheckerScreen::new(ui.clone(), Some(tx))))
.await;
if matches!(result, UserChoice::AcceptAll) {
file_state.accepted_tos = true;
file_state.accepted_pp = true;
file_state.tos = Some(current_tos);
file_state.privacy = Some(current_privacy);
let result = rx.await.unwrap_or(UserChoice::Deny);
match result {
UserChoice::AcceptEULA | UserChoice::AcceptAll => {
if let Some((current_eula, current_tos, current_privacy)) =
get_current_docs().await
{
state.accepted_eula = true;
state.eula = Some(current_eula);
if matches!(result, UserChoice::AcceptAll) {
state.accepted_tos = true;
state.accepted_pp = true;
state.tos = Some(current_tos);
state.privacy = Some(current_privacy);
}
}
}
UserChoice::Deny => return (false, false),
}
UserChoice::Deny => {
return (false, false);
}
save_file("", "agreements", &state.to_string());
}
save_file("", "agreements", &file_state.to_string());
if let Some((eula_update, tos_update, privacy_update)) = Self::get_updates().await {
let _is_forced = matches!(eula_update, UpdateDecision::Forced(_))
let is_forced = matches!(eula_update, UpdateDecision::Forced(_))
|| matches!(tos_update, UpdateDecision::Forced(_))
|| matches!(privacy_update, UpdateDecision::Forced(_));
if let Some((eula_update, tos_update, privacy_update)) = Self::get_updates().await {
let is_forced = matches!(eula_update, UpdateDecision::Forced(_))
|| matches!(tos_update, UpdateDecision::Forced(_))
|| matches!(privacy_update, UpdateDecision::Forced(_));
let (tx, rx) = oneshot::channel();
let (tx, rx) = oneshot::channel();
ui.set_screen(Box::new(TermsUpdaterScreen::new(
ui.clone(),
eula_update.clone(),
tos_update.clone(),
privacy_update.clone(),
Some(tx),
)))
.await;
ui.set_screen(Box::new(TermsUpdaterScreen::new(
ui.clone(),
eula_update.clone(),
tos_update.clone(),
privacy_update.clone(),
Some(tx),
)))
.await;
let result = rx.await.unwrap_or(UserChoice::Deny);
if is_forced {
let result = rx.await.unwrap_or(UserChoice::Deny);
let file = load_file("", "agreements");
let mut state = ConsentState::from_str(&file).sanitize();
match result {
UserChoice::AcceptAll => {
state.accepted_eula = true;
state.accepted_tos = true;
state.accepted_pp = true;
if is_forced {
match result {
UserChoice::AcceptAll => {
state.accepted_eula = true;
state.accepted_tos = true;
state.accepted_pp = true;
}
UserChoice::AcceptEULA => {
state.accepted_eula = true;
}
UserChoice::Deny => return (false, false),
}
} else {
match result {
UserChoice::AcceptAll => {
if let UpdateDecision::Future { newest } = eula_update {
state.future_eula = Some(newest);
}
UserChoice::AcceptEULA => {
state.accepted_eula = true;
if let UpdateDecision::Future { newest } = tos_update {
state.future_tos = Some(newest);
}
UserChoice::Deny => {
return (false, false);
if let UpdateDecision::Future { newest } = privacy_update {
state.future_privacy = Some(newest);
}
}
let state = state.sanitize();
save_file("", "agreements", &state.to_string());
} else {
tokio::spawn(async move {
let result = rx.await.unwrap_or(UserChoice::Deny);
let file = load_file("", "agreements");
let mut state = ConsentState::from_str(&file).sanitize();
match result {
UserChoice::AcceptAll => {
state.future_eula = match eula_update {
UpdateDecision::Future { newest } => Some(newest),
_ => state.future_eula,
};
state.future_tos = match tos_update {
UpdateDecision::Future { newest } => Some(newest),
_ => state.future_tos,
};
state.future_privacy = match privacy_update {
UpdateDecision::Future { newest } => Some(newest),
_ => state.future_privacy,
};
}
UserChoice::AcceptEULA => {
if let UpdateDecision::Future { newest } = eula_update {
state.future_eula = Some(newest);
}
}
UserChoice::Deny => {}
UserChoice::AcceptEULA => {
if let UpdateDecision::Future { newest } = eula_update {
state.future_eula = Some(newest);
}
let state = state.sanitize();
save_file("", "agreements", &state.to_string());
});
}
UserChoice::Deny => {}
}
}
state = state.sanitize();
save_file("", "agreements", &state.to_string());
}
let file = load_file("", "agreements");
let final_state = ConsentState::from_str(&file).sanitize();
state = ConsentState::from_str(&load_file("", "agreements")).sanitize();
save_file("", "agreements", &state.to_string());
save_file("", "agreements", &final_state.to_string());
(
final_state.accepted_eula,
final_state.accepted_tos && final_state.accepted_pp,
)
(state.accepted_eula, state.accepted_tos && state.accepted_pp)
}
async fn get_updates() -> Option<(