[WIP] Consent UI, via main UI render System
This commit is contained in:
parent
6db4e806f8
commit
a0fddf723f
18 changed files with 1625 additions and 1040 deletions
16
logs/log_1771421284.txt
Normal file
16
logs/log_1771421284.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
1771421284869 IOTA ID: 0
|
||||||
|
1771421284869 User IDS:
|
||||||
|
1771421284885 Community IDS:
|
||||||
|
1771421284886 TLS certificate 'certs/cert.pem' not found.
|
||||||
|
1771421284886 HTTP Server running on 0.0.0.0:1984
|
||||||
|
1771421284887 Communities active on ws://10.209.226.34:1984/community/...
|
||||||
|
1771421284887 Downloading ZIP file...
|
||||||
|
1771421285166 Failed to download file: Status 404 Not Found
|
||||||
|
1771421285167 Error downloading file: Failed to download file: Status 404 Not Found
|
||||||
|
1771421285167 IOTA_REGISTER_NEW
|
||||||
|
1771421285173 OMIKRON_CONNECTING
|
||||||
|
1771421285459 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771421297984 Shutdown signal received.
|
||||||
|
1771421297985 Web Server shutdown complete.
|
||||||
|
1771421305461 Request timed out after 20 seconds.
|
||||||
|
1771421305461 Launched
|
||||||
42
logs/log_1771421408.txt
Normal file
42
logs/log_1771421408.txt
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
1771421408357 IOTA ID: 0
|
||||||
|
1771421408357 User IDS:
|
||||||
|
1771421408369 Community IDS:
|
||||||
|
1771421408369 TLS certificate 'certs/cert.pem' not found.
|
||||||
|
1771421408369 HTTP Server running on 0.0.0.0:1984
|
||||||
|
1771421408370 Communities active on ws://10.209.226.34:1984/community/...
|
||||||
|
1771421408370 Downloading ZIP file...
|
||||||
|
1771421408574 Failed to download file: Status 404 Not Found
|
||||||
|
1771421408574 Error downloading file: Failed to download file: Status 404 Not Found
|
||||||
|
1771421408574 IOTA_REGISTER_NEW
|
||||||
|
1771421408581 OMIKRON_CONNECTING
|
||||||
|
1771421408791 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771421428793 Request timed out after 20 seconds.
|
||||||
|
1771421428793 Launched
|
||||||
|
1771421428824 error_internal
|
||||||
|
1771421428911 Connection closed.
|
||||||
|
1771421429796 IOTA_REGISTER_NEW
|
||||||
|
1771421429800 OMIKRON_CONNECTING
|
||||||
|
1771421430102 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771421450104 Request timed out after 20 seconds.
|
||||||
|
1771421450106 Launched
|
||||||
|
1771421450135 error_internal
|
||||||
|
1771421450205 Connection closed.
|
||||||
|
1771421451108 IOTA_REGISTER_NEW
|
||||||
|
1771421451113 OMIKRON_CONNECTING
|
||||||
|
1771421451310 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771421471312 Request timed out after 20 seconds.
|
||||||
|
1771421471314 Launched
|
||||||
|
1771421471354 error_internal
|
||||||
|
1771421471428 Connection closed.
|
||||||
|
1771421472315 IOTA_REGISTER_NEW
|
||||||
|
1771421472319 OMIKRON_CONNECTING
|
||||||
|
1771421472543 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771421492544 Request timed out after 20 seconds.
|
||||||
|
1771421492546 Launched
|
||||||
|
1771421492568 error_internal
|
||||||
|
1771421492597 Connection closed.
|
||||||
|
1771421493548 IOTA_REGISTER_NEW
|
||||||
|
1771421493554 OMIKRON_CONNECTING
|
||||||
|
1771421493769 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771421493828 Shutdown signal received.
|
||||||
|
1771421493830 Web Server shutdown complete.
|
||||||
15
logs/log_1771421600.txt
Normal file
15
logs/log_1771421600.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
1771421600740 IOTA ID: 0
|
||||||
|
1771421600740 User IDS:
|
||||||
|
1771421600755 Community IDS:
|
||||||
|
1771421600756 TLS certificate 'certs/cert.pem' not found.
|
||||||
|
1771421600756 HTTP Server running on 0.0.0.0:1984
|
||||||
|
1771421600756 Communities active on ws://10.209.226.34:1984/community/...
|
||||||
|
1771421600756 Downloading ZIP file...
|
||||||
|
1771421600982 Failed to download file: Status 404 Not Found
|
||||||
|
1771421600982 Error downloading file: Failed to download file: Status 404 Not Found
|
||||||
|
1771421600983 IOTA_REGISTER_NEW
|
||||||
|
1771421600988 OMIKRON_CONNECTING
|
||||||
|
1771421601270 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771421606464 Shutdown signal received.
|
||||||
|
1771421621272 Request timed out after 20 seconds.
|
||||||
|
1771421621272 Launched
|
||||||
184
logs/log_1771423051.txt
Normal file
184
logs/log_1771423051.txt
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
1771423051538 IOTA ID: 0
|
||||||
|
1771423051538 User IDS:
|
||||||
|
1771423051551 Community IDS:
|
||||||
|
1771423051551 TLS certificate 'certs/cert.pem' not found.
|
||||||
|
1771423051551 HTTP Server running on 0.0.0.0:1984
|
||||||
|
1771423051552 Communities active on ws://10.209.226.34:1984/community/...
|
||||||
|
1771423051552 Downloading ZIP file...
|
||||||
|
1771423051790 Failed to download file: Status 404 Not Found
|
||||||
|
1771423051790 Error downloading file: Failed to download file: Status 404 Not Found
|
||||||
|
1771423051790 IOTA_REGISTER_NEW
|
||||||
|
1771423051793 OMIKRON_CONNECTING
|
||||||
|
1771423052032 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423072035 Request timed out after 20 seconds.
|
||||||
|
1771423072035 Launched
|
||||||
|
1771423072067 error_internal
|
||||||
|
1771423072105 Connection closed.
|
||||||
|
1771423073036 IOTA_REGISTER_NEW
|
||||||
|
1771423073040 OMIKRON_CONNECTING
|
||||||
|
1771423073256 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423093258 Request timed out after 20 seconds.
|
||||||
|
1771423093262 Launched
|
||||||
|
1771423093286 error_internal
|
||||||
|
1771423093322 Connection closed.
|
||||||
|
1771423094264 IOTA_REGISTER_NEW
|
||||||
|
1771423094267 OMIKRON_CONNECTING
|
||||||
|
1771423094484 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423114491 Request timed out after 20 seconds.
|
||||||
|
1771423114492 Launched
|
||||||
|
1771423114520 error_internal
|
||||||
|
1771423114558 Connection closed.
|
||||||
|
1771423115495 IOTA_REGISTER_NEW
|
||||||
|
1771423115501 OMIKRON_CONNECTING
|
||||||
|
1771423115724 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423135725 Request timed out after 20 seconds.
|
||||||
|
1771423135728 Launched
|
||||||
|
1771423135759 error_internal
|
||||||
|
1771423135794 Connection closed.
|
||||||
|
1771423136730 IOTA_REGISTER_NEW
|
||||||
|
1771423136738 OMIKRON_CONNECTING
|
||||||
|
1771423136959 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423156960 Request timed out after 20 seconds.
|
||||||
|
1771423156963 Launched
|
||||||
|
1771423156999 error_internal
|
||||||
|
1771423157078 Connection closed.
|
||||||
|
1771423157965 IOTA_REGISTER_NEW
|
||||||
|
1771423157970 OMIKRON_CONNECTING
|
||||||
|
1771423158169 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423178171 Request timed out after 20 seconds.
|
||||||
|
1771423178173 Launched
|
||||||
|
1771423178200 error_internal
|
||||||
|
1771423178270 Connection closed.
|
||||||
|
1771423179174 IOTA_REGISTER_NEW
|
||||||
|
1771423179178 OMIKRON_CONNECTING
|
||||||
|
1771423179404 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423199406 Request timed out after 20 seconds.
|
||||||
|
1771423199409 Launched
|
||||||
|
1771423199437 error_internal
|
||||||
|
1771423199485 Connection closed.
|
||||||
|
1771423200413 IOTA_REGISTER_NEW
|
||||||
|
1771423200418 OMIKRON_CONNECTING
|
||||||
|
1771423200855 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423220858 Request timed out after 20 seconds.
|
||||||
|
1771423220861 Launched
|
||||||
|
1771423220884 error_internal
|
||||||
|
1771423220913 Connection closed.
|
||||||
|
1771423221864 IOTA_REGISTER_NEW
|
||||||
|
1771423221870 OMIKRON_CONNECTING
|
||||||
|
1771423222087 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423242092 Request timed out after 20 seconds.
|
||||||
|
1771423242095 Launched
|
||||||
|
1771423242115 error_internal
|
||||||
|
1771423242145 Connection closed.
|
||||||
|
1771423243096 IOTA_REGISTER_NEW
|
||||||
|
1771423243101 OMIKRON_CONNECTING
|
||||||
|
1771423243328 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423263334 Request timed out after 20 seconds.
|
||||||
|
1771423263337 Launched
|
||||||
|
1771423263363 error_internal
|
||||||
|
1771423263412 Connection closed.
|
||||||
|
1771423264343 IOTA_REGISTER_NEW
|
||||||
|
1771423264348 OMIKRON_CONNECTING
|
||||||
|
1771423264553 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423284555 Request timed out after 20 seconds.
|
||||||
|
1771423284558 Launched
|
||||||
|
1771423284582 error_internal
|
||||||
|
1771423284609 Connection closed.
|
||||||
|
1771423285560 IOTA_REGISTER_NEW
|
||||||
|
1771423285563 OMIKRON_CONNECTING
|
||||||
|
1771423285770 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423305772 Request timed out after 20 seconds.
|
||||||
|
1771423305775 Launched
|
||||||
|
1771423305801 error_internal
|
||||||
|
1771423305837 Connection closed.
|
||||||
|
1771423306776 IOTA_REGISTER_NEW
|
||||||
|
1771423306786 OMIKRON_CONNECTING
|
||||||
|
1771423306995 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423326998 Request timed out after 20 seconds.
|
||||||
|
1771423327000 Launched
|
||||||
|
1771423327022 error_internal
|
||||||
|
1771423327068 Connection closed.
|
||||||
|
1771423328002 IOTA_REGISTER_NEW
|
||||||
|
1771423328006 OMIKRON_CONNECTING
|
||||||
|
1771423328253 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423348256 Request timed out after 20 seconds.
|
||||||
|
1771423348259 Launched
|
||||||
|
1771423348298 error_internal
|
||||||
|
1771423348335 Connection closed.
|
||||||
|
1771423349260 IOTA_REGISTER_NEW
|
||||||
|
1771423349265 OMIKRON_CONNECTING
|
||||||
|
1771423349504 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423369505 Request timed out after 20 seconds.
|
||||||
|
1771423369507 Launched
|
||||||
|
1771423369535 error_internal
|
||||||
|
1771423369584 Connection closed.
|
||||||
|
1771423370508 IOTA_REGISTER_NEW
|
||||||
|
1771423370512 OMIKRON_CONNECTING
|
||||||
|
1771423370754 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423390756 Request timed out after 20 seconds.
|
||||||
|
1771423390759 Launched
|
||||||
|
1771423390800 error_internal
|
||||||
|
1771423390827 Connection closed.
|
||||||
|
1771423391760 IOTA_REGISTER_NEW
|
||||||
|
1771423391764 OMIKRON_CONNECTING
|
||||||
|
1771423392002 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423412005 Request timed out after 20 seconds.
|
||||||
|
1771423412008 Launched
|
||||||
|
1771423412038 error_internal
|
||||||
|
1771423412065 Connection closed.
|
||||||
|
1771423413009 IOTA_REGISTER_NEW
|
||||||
|
1771423413015 OMIKRON_CONNECTING
|
||||||
|
1771423413208 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423433210 Request timed out after 20 seconds.
|
||||||
|
1771423433212 Launched
|
||||||
|
1771423433234 error_internal
|
||||||
|
1771423433271 Connection closed.
|
||||||
|
1771423434215 IOTA_REGISTER_NEW
|
||||||
|
1771423434221 OMIKRON_CONNECTING
|
||||||
|
1771423434422 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423454425 Request timed out after 20 seconds.
|
||||||
|
1771423454426 Launched
|
||||||
|
1771423454448 error_internal
|
||||||
|
1771423454478 Connection closed.
|
||||||
|
1771423455428 IOTA_REGISTER_NEW
|
||||||
|
1771423455433 OMIKRON_CONNECTING
|
||||||
|
1771423455707 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423475712 Request timed out after 20 seconds.
|
||||||
|
1771423475716 Launched
|
||||||
|
1771423475748 error_internal
|
||||||
|
1771423475779 Connection closed.
|
||||||
|
1771423476717 IOTA_REGISTER_NEW
|
||||||
|
1771423476721 OMIKRON_CONNECTING
|
||||||
|
1771423476935 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423496936 Request timed out after 20 seconds.
|
||||||
|
1771423496938 Launched
|
||||||
|
1771423496970 error_internal
|
||||||
|
1771423497007 Connection closed.
|
||||||
|
1771423497939 IOTA_REGISTER_NEW
|
||||||
|
1771423497945 OMIKRON_CONNECTING
|
||||||
|
1771423498170 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423518171 Request timed out after 20 seconds.
|
||||||
|
1771423518174 Launched
|
||||||
|
1771423518201 error_internal
|
||||||
|
1771423518241 Connection closed.
|
||||||
|
1771423519176 IOTA_REGISTER_NEW
|
||||||
|
1771423519181 OMIKRON_CONNECTING
|
||||||
|
1771423519392 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423539396 Request timed out after 20 seconds.
|
||||||
|
1771423539398 Launched
|
||||||
|
1771423539424 error_internal
|
||||||
|
1771423539455 Connection closed.
|
||||||
|
1771423540400 IOTA_REGISTER_NEW
|
||||||
|
1771423540404 OMIKRON_CONNECTING
|
||||||
|
1771423540643 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423560644 Request timed out after 20 seconds.
|
||||||
|
1771423560647 Launched
|
||||||
|
1771423560687 error_internal
|
||||||
|
1771423560747 Connection closed.
|
||||||
|
1771423561649 IOTA_REGISTER_NEW
|
||||||
|
1771423561653 OMIKRON_CONNECTING
|
||||||
|
1771423561858 OMIKRON_CONNECTION_SUCCESS
|
||||||
|
1771423574596 Shutdown signal received.
|
||||||
|
1771423574597 Web Server shutdown complete.
|
||||||
|
1771423581860 Request timed out after 20 seconds.
|
||||||
|
1771423581864 Launched
|
||||||
11
logs/log_1771423595.txt
Normal file
11
logs/log_1771423595.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
1771423595203 IOTA ID: 0
|
||||||
|
1771423595203 User IDS:
|
||||||
|
1771423595215 Community IDS:
|
||||||
|
1771423595216 TLS certificate 'certs/cert.pem' not found.
|
||||||
|
1771423595216 HTTP Server running on 0.0.0.0:1984
|
||||||
|
1771423595217 Communities active on ws://10.209.226.34:1984/community/...
|
||||||
|
1771423595217 Downloading ZIP file...
|
||||||
|
1771423608345 Shutdown signal received.
|
||||||
|
1771423608346 Web Server shutdown complete.
|
||||||
|
1771423685485 Failed to download file: Status 504 Gateway Timeout
|
||||||
|
1771423685485 Error downloading file: Failed to download file: Status 504 Gateway Timeout
|
||||||
|
|
@ -10,7 +10,6 @@ use ratatui::{
|
||||||
widgets::{Block, Borders, Paragraph, Wrap},
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
};
|
};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UiLogEntry {
|
pub struct UiLogEntry {
|
||||||
|
|
@ -89,13 +88,13 @@ impl InteractableElement for LogCard {
|
||||||
if self.scroll > 0 {
|
if self.scroll > 0 {
|
||||||
self.scroll -= 1;
|
self.scroll -= 1;
|
||||||
}
|
}
|
||||||
InteractionResult::Handeled
|
InteractionResult::Handled
|
||||||
}
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
self.scroll += 1;
|
self.scroll += 1;
|
||||||
InteractionResult::Handeled
|
InteractionResult::Handled
|
||||||
}
|
}
|
||||||
_ => InteractionResult::Unhandeled,
|
_ => InteractionResult::Unhandled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
66
src/gui/input_handler.rs
Normal file
66
src/gui/input_handler.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
use crate::ACTIVE_TASKS;
|
||||||
|
use crate::gui::ui::UI;
|
||||||
|
use crate::{RELOAD, SHUTDOWN};
|
||||||
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub fn setup_input_handler(ui: Arc<UI>) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
ACTIVE_TASKS
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push("Input Handler".to_string());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let should_shutdown = *SHUTDOWN.read().await;
|
||||||
|
if should_shutdown {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_event = match poll(Duration::from_millis(100)) {
|
||||||
|
Ok(true) => true,
|
||||||
|
Ok(false) => false,
|
||||||
|
Err(_) => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if has_event {
|
||||||
|
match read() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut tasks = ACTIVE_TASKS.lock().unwrap();
|
||||||
|
tasks.retain(|t| t != "Input Handler");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_input(key: KeyEvent, ui: Arc<UI>) {
|
||||||
|
match (key.code, key.modifiers) {
|
||||||
|
(KeyCode::Char('q'), KeyModifiers::CONTROL) => {
|
||||||
|
*SHUTDOWN.write().await = true;
|
||||||
|
}
|
||||||
|
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
|
||||||
|
*SHUTDOWN.write().await = true;
|
||||||
|
}
|
||||||
|
(KeyCode::Char('r'), KeyModifiers::CONTROL) => {
|
||||||
|
*RELOAD.write().await = true;
|
||||||
|
*SHUTDOWN.write().await = true;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
ui.handle_input(key).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,14 +5,15 @@ use std::pin::Pin;
|
||||||
use crate::gui::screens::screens::Screen;
|
use crate::gui::screens::screens::Screen;
|
||||||
|
|
||||||
pub enum InteractionResult {
|
pub enum InteractionResult {
|
||||||
|
CloseScreen,
|
||||||
OpenScreen {
|
OpenScreen {
|
||||||
screen: Box<dyn Screen>,
|
screen: Box<dyn Screen>,
|
||||||
},
|
},
|
||||||
OpenFutureScreen {
|
OpenFutureScreen {
|
||||||
screen: Pin<Box<dyn Future<Output = Box<dyn Screen>> + Send>>,
|
screen: Pin<Box<dyn Future<Output = Box<dyn Screen>> + Send>>,
|
||||||
},
|
},
|
||||||
Handeled,
|
Handled,
|
||||||
Unhandeled,
|
Unhandled,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for InteractionResult {
|
impl Debug for InteractionResult {
|
||||||
|
|
@ -20,8 +21,9 @@ impl Debug for InteractionResult {
|
||||||
match self {
|
match self {
|
||||||
InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"),
|
InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"),
|
||||||
InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"),
|
InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"),
|
||||||
InteractionResult::Handeled => write!(f, "Handeled"),
|
InteractionResult::CloseScreen => write!(f, "CloseScreen"),
|
||||||
InteractionResult::Unhandeled => write!(f, "Unhandeled"),
|
InteractionResult::Handled => write!(f, "Handled"),
|
||||||
|
InteractionResult::Unhandled => write!(f, "Unhandled"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ pub mod elements {
|
||||||
}
|
}
|
||||||
pub mod screens {
|
pub mod screens {
|
||||||
pub mod screens;
|
pub mod screens;
|
||||||
|
pub mod terms_checker;
|
||||||
|
pub mod terms_updater;
|
||||||
}
|
}
|
||||||
pub mod app_state;
|
pub mod app_state;
|
||||||
|
pub mod input_handler;
|
||||||
pub mod interaction_result;
|
pub mod interaction_result;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
|
||||||
353
src/gui/screens/terms_checker.rs
Normal file
353
src/gui/screens/terms_checker.rs
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
use crate::{
|
||||||
|
gui::{interaction_result::InteractionResult, screens::screens::Screen, ui::UI},
|
||||||
|
terms::{
|
||||||
|
buttons::{checkbox, draw_buttons},
|
||||||
|
consent_state::UserChoice,
|
||||||
|
focus::Focus,
|
||||||
|
md_viewer::FileViewer,
|
||||||
|
terms_getter::{Type, get_link, get_terms},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
text::{Line, Span, Text},
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
|
};
|
||||||
|
use std::{any::Any, pin::Pin, sync::Arc};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
pub struct TermsCheckerScreen {
|
||||||
|
ui: Arc<UI>,
|
||||||
|
sender: Option<oneshot::Sender<UserChoice>>,
|
||||||
|
|
||||||
|
eula: bool,
|
||||||
|
tos: bool,
|
||||||
|
pp: bool,
|
||||||
|
|
||||||
|
focus: Focus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TermsCheckerScreen {
|
||||||
|
pub fn new(ui: Arc<UI>, sender: Option<oneshot::Sender<UserChoice>>) -> Self {
|
||||||
|
Self {
|
||||||
|
ui,
|
||||||
|
sender,
|
||||||
|
eula: false,
|
||||||
|
tos: false,
|
||||||
|
pp: false,
|
||||||
|
focus: Focus::Eula,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Screen for TermsCheckerScreen {
|
||||||
|
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, size: Rect) {
|
||||||
|
let mut needed_height = 5;
|
||||||
|
|
||||||
|
if size.height < 6 || size.width < 27 {
|
||||||
|
f.render_widget(
|
||||||
|
Line::from(format!("too small {}/63 by {}/13", size.width, size.height)),
|
||||||
|
size,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_width = 150;
|
||||||
|
let max_height = 26;
|
||||||
|
|
||||||
|
let content_width = if max_width < size.width {
|
||||||
|
max_width
|
||||||
|
} else {
|
||||||
|
size.width
|
||||||
|
};
|
||||||
|
let content_height = if max_height < size.height {
|
||||||
|
max_height
|
||||||
|
} else {
|
||||||
|
size.height
|
||||||
|
};
|
||||||
|
|
||||||
|
let horizontal_margin = (size.width.saturating_sub(content_width)) / 2;
|
||||||
|
let vertical_margin = (size.height.saturating_sub(content_height)) / 2;
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(7), Constraint::Length(3)])
|
||||||
|
.split(Rect {
|
||||||
|
x: horizontal_margin,
|
||||||
|
y: vertical_margin,
|
||||||
|
width: content_width,
|
||||||
|
height: content_height,
|
||||||
|
});
|
||||||
|
let eula_text = if size.width < 70 {
|
||||||
|
"EULA ¹ (https://legal.tensamin.net/eula/)"
|
||||||
|
} else {
|
||||||
|
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)"
|
||||||
|
};
|
||||||
|
let tos_text = if size.width < 72 {
|
||||||
|
"ToS ² (https://legal.tensamin.net/terms-of-service/)"
|
||||||
|
} else {
|
||||||
|
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)"
|
||||||
|
};
|
||||||
|
let pp_text = if size.width < 68 {
|
||||||
|
"PP ² (https://legal.tensamin.net/privacy-policy/)"
|
||||||
|
} else {
|
||||||
|
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)"
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) = if size.width > 143 {
|
||||||
|
(
|
||||||
|
vec![8, 3, 8, 5],
|
||||||
|
vec![
|
||||||
|
"",
|
||||||
|
"By selecting Continue, you confirm that you agree to the End User License Agreement and applicable Terms of Service.",
|
||||||
|
"",
|
||||||
|
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
|
||||||
|
"",
|
||||||
|
"While having a document selected press O to view in this UI or press L to open as a link.",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else if size.width > 92 {
|
||||||
|
(
|
||||||
|
vec![9, 3, 9, 5],
|
||||||
|
vec![
|
||||||
|
"",
|
||||||
|
"By selecting Continue, you confirm that you agree to the End User License",
|
||||||
|
"Agreement and applicable Terms of Service.",
|
||||||
|
"",
|
||||||
|
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
|
||||||
|
"",
|
||||||
|
"While having a document selected press O to view in this UI or press L to open as a link.",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else if size.width > 73 {
|
||||||
|
(
|
||||||
|
vec![9, 3, 9, 5],
|
||||||
|
vec![
|
||||||
|
"",
|
||||||
|
"By selecting Continue, you confirm that you agree to the",
|
||||||
|
"End User License Agreement and applicable Terms of Service.",
|
||||||
|
"",
|
||||||
|
"Tensamin services require acceptance of the ToS and Privacy Policy.",
|
||||||
|
"",
|
||||||
|
"While having a document selected press O to view in this UI or press L",
|
||||||
|
"to open as a link.",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
vec![10, 3, 11, 5],
|
||||||
|
vec![
|
||||||
|
"",
|
||||||
|
"By selecting Continue, you confirm that you agree",
|
||||||
|
"to the End User License Agreement and",
|
||||||
|
"applicable Terms of Service.",
|
||||||
|
"",
|
||||||
|
"Tensamin services require acceptance of the",
|
||||||
|
"Terms of Service and Privacy Policy.",
|
||||||
|
"",
|
||||||
|
"While having a document selected press O to view",
|
||||||
|
"in this UI or press L to open as a link.",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut text_lines = vec![
|
||||||
|
checkbox(eula_text, self.eula, self.focus == Focus::Eula, true),
|
||||||
|
checkbox(tos_text, self.tos, self.focus == Focus::Tos, self.eula),
|
||||||
|
checkbox(pp_text, self.pp, self.focus == Focus::Pp, self.eula),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from("¹ Necessary– required to run the program"),
|
||||||
|
Line::from("² Optional – required only for Tensamin services"),
|
||||||
|
];
|
||||||
|
for line in agree_lines {
|
||||||
|
text_lines.insert(text_lines.len(), Line::from(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
while size.height - 5 < text_lines.len() as u16 {
|
||||||
|
if optional_lines.len() == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
text_lines.remove(optional_lines[0] as usize);
|
||||||
|
optional_lines.remove(0);
|
||||||
|
}
|
||||||
|
needed_height += text_lines.len();
|
||||||
|
|
||||||
|
if size.width < 60 || size.height < needed_height as u16 {
|
||||||
|
let width_style = if size.width > 76 {
|
||||||
|
Style::default().fg(Color::Green)
|
||||||
|
} else if size.width >= 60 {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Red)
|
||||||
|
};
|
||||||
|
|
||||||
|
let height_style = if size.height > 19 {
|
||||||
|
Style::default().fg(Color::Green)
|
||||||
|
} else if size.height >= 13 {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Red)
|
||||||
|
};
|
||||||
|
|
||||||
|
let warning_text = Text::from(vec![
|
||||||
|
Line::from(vec![
|
||||||
|
Span::raw("Width: "),
|
||||||
|
Span::styled(format!("{}", size.width), width_style),
|
||||||
|
Span::raw(" / 60"),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::raw("Height: "),
|
||||||
|
Span::styled(format!("{}", size.height), height_style),
|
||||||
|
Span::raw(format!(" / 13")),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let warning = Paragraph::new(warning_text)
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(format!("UI Too Small [Q to Quit]")),
|
||||||
|
);
|
||||||
|
f.render_widget(warning, size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let consent_block = Paragraph::new(Text::from(text_lines)).block(
|
||||||
|
Block::default()
|
||||||
|
.title(format!(" Tensamin User Consent [Q to Quit] ",))
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
);
|
||||||
|
f.render_widget(consent_block, chunks[0]);
|
||||||
|
|
||||||
|
draw_buttons(
|
||||||
|
f,
|
||||||
|
chunks[1],
|
||||||
|
self.focus,
|
||||||
|
(self.eula, self.tos && self.pp),
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
|
||||||
|
let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel];
|
||||||
|
|
||||||
|
if self.eula {
|
||||||
|
possible_states.push(Focus::Continue);
|
||||||
|
if self.tos && self.pp {
|
||||||
|
possible_states.push(Focus::ContinueAll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match event.code {
|
||||||
|
KeyCode::Esc => {
|
||||||
|
if let Some(sender) = self.sender.take() {
|
||||||
|
let _ = sender.send(UserChoice::Deny);
|
||||||
|
}
|
||||||
|
InteractionResult::CloseScreen
|
||||||
|
}
|
||||||
|
KeyCode::Up | KeyCode::Left => {
|
||||||
|
self.focus.prev(&possible_states);
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
KeyCode::Down | KeyCode::Right | KeyCode::Tab => {
|
||||||
|
self.focus.next(&possible_states);
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
KeyCode::Char('q') | KeyCode::Char('Q') => {
|
||||||
|
if let Some(sender) = self.sender.take() {
|
||||||
|
let _ = sender.send(UserChoice::Deny);
|
||||||
|
}
|
||||||
|
InteractionResult::CloseScreen
|
||||||
|
}
|
||||||
|
KeyCode::Char('o') | KeyCode::Char('O') => {
|
||||||
|
let terms_type = match self.focus {
|
||||||
|
Focus::Eula => Some(Type::EULA),
|
||||||
|
Focus::Tos => Some(Type::TOS),
|
||||||
|
Focus::Pp => Some(Type::PP),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(terms_type) = terms_type {
|
||||||
|
let ui = self.ui.clone();
|
||||||
|
let fut: Pin<Box<dyn Future<Output = Box<dyn Screen>> + Send>> =
|
||||||
|
Box::pin(async move {
|
||||||
|
let content = get_terms(terms_type.clone()).await.unwrap();
|
||||||
|
let screen: FileViewer =
|
||||||
|
FileViewer::new(ui.clone(), terms_type.to_string(), &content);
|
||||||
|
Box::new(screen) as Box<dyn Screen>
|
||||||
|
});
|
||||||
|
InteractionResult::OpenFutureScreen { screen: fut }
|
||||||
|
} else {
|
||||||
|
InteractionResult::Unhandled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('l') | KeyCode::Char('L') => match self.focus {
|
||||||
|
Focus::Eula => {
|
||||||
|
let _ = open::that(get_link(Type::EULA));
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
Focus::Tos => {
|
||||||
|
let _ = open::that(get_link(Type::TOS));
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
Focus::Pp => {
|
||||||
|
let _ = open::that(get_link(Type::PP));
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
_ => InteractionResult::Unhandled,
|
||||||
|
},
|
||||||
|
KeyCode::Enter | KeyCode::Char(' ') => match self.focus {
|
||||||
|
Focus::Eula => {
|
||||||
|
self.eula = !self.eula;
|
||||||
|
self.tos = false;
|
||||||
|
self.pp = false;
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
Focus::Tos if self.eula => {
|
||||||
|
self.tos = !self.tos;
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
Focus::Pp if self.eula => {
|
||||||
|
self.pp = !self.pp;
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
Focus::Cancel => {
|
||||||
|
if let Some(sender) = self.sender.take() {
|
||||||
|
let _ = sender.send(UserChoice::Deny);
|
||||||
|
}
|
||||||
|
InteractionResult::CloseScreen
|
||||||
|
}
|
||||||
|
Focus::Continue if self.eula => {
|
||||||
|
if let Some(sender) = self.sender.take() {
|
||||||
|
let _ = sender.send(UserChoice::AcceptEULA);
|
||||||
|
}
|
||||||
|
InteractionResult::CloseScreen
|
||||||
|
}
|
||||||
|
Focus::ContinueAll if self.eula && self.tos && self.pp => {
|
||||||
|
if let Some(sender) = self.sender.take() {
|
||||||
|
let _ = sender.send(UserChoice::AcceptAll);
|
||||||
|
}
|
||||||
|
InteractionResult::CloseScreen
|
||||||
|
}
|
||||||
|
_ => InteractionResult::Unhandled,
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => InteractionResult::Unhandled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
701
src/gui/screens/terms_updater.rs
Normal file
701
src/gui/screens/terms_updater.rs
Normal file
|
|
@ -0,0 +1,701 @@
|
||||||
|
use crate::{
|
||||||
|
gui::{interaction_result::InteractionResult, screens::screens::Screen, ui::UI},
|
||||||
|
terms::{
|
||||||
|
buttons::{checkbox, draw_buttons},
|
||||||
|
consent_state::{UpdateDecision, UserChoice},
|
||||||
|
doc::Doc,
|
||||||
|
focus::Focus,
|
||||||
|
md_viewer::FileViewer,
|
||||||
|
terms_getter::{Type, get_link, get_terms},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use chrono::{Local, TimeZone, Utc};
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
text::{Line, Span, Text},
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
|
};
|
||||||
|
use std::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
pub struct TermsUpdaterScreen {
|
||||||
|
ui: Arc<UI>,
|
||||||
|
sender: Option<oneshot::Sender<UserChoice>>,
|
||||||
|
|
||||||
|
eula_needed: bool,
|
||||||
|
tos_needed: bool,
|
||||||
|
pp_needed: bool,
|
||||||
|
|
||||||
|
eula_future: bool,
|
||||||
|
tos_future: bool,
|
||||||
|
pp_future: bool,
|
||||||
|
|
||||||
|
eula_for_future: Option<Doc>,
|
||||||
|
tos_for_future: Option<Doc>,
|
||||||
|
pp_for_future: Option<Doc>,
|
||||||
|
|
||||||
|
update_needed: bool,
|
||||||
|
|
||||||
|
eula: bool,
|
||||||
|
tos: bool,
|
||||||
|
pp: bool,
|
||||||
|
|
||||||
|
focus: Focus,
|
||||||
|
}
|
||||||
|
impl TermsUpdaterScreen {
|
||||||
|
pub fn new(
|
||||||
|
ui: Arc<UI>,
|
||||||
|
consent_eula: UpdateDecision,
|
||||||
|
consent_tos: UpdateDecision,
|
||||||
|
consent_pp: UpdateDecision,
|
||||||
|
sender: Option<oneshot::Sender<UserChoice>>,
|
||||||
|
) -> Self {
|
||||||
|
let (eula_needed, eula_future, eula_for_future): (bool, bool, Option<Doc>) =
|
||||||
|
match consent_eula {
|
||||||
|
UpdateDecision::NoChange => (false, false, None),
|
||||||
|
UpdateDecision::Future { newest } => (true, true, Some(newest)),
|
||||||
|
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
|
||||||
|
};
|
||||||
|
let (tos_needed, tos_future, tos_for_future): (bool, bool, Option<Doc>) = match consent_tos
|
||||||
|
{
|
||||||
|
UpdateDecision::NoChange => (false, false, None),
|
||||||
|
UpdateDecision::Future { newest } => (true, true, Some(newest)),
|
||||||
|
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
|
||||||
|
};
|
||||||
|
let (pp_needed, pp_future, pp_for_future): (bool, bool, Option<Doc>) = match consent_pp {
|
||||||
|
UpdateDecision::NoChange => (false, false, None),
|
||||||
|
UpdateDecision::Future { newest } => (true, true, Some(newest)),
|
||||||
|
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let focus = if eula_needed {
|
||||||
|
Focus::Eula
|
||||||
|
} else if tos_needed {
|
||||||
|
Focus::Tos
|
||||||
|
} else if pp_needed {
|
||||||
|
Focus::Pp
|
||||||
|
} else {
|
||||||
|
Focus::Cancel
|
||||||
|
};
|
||||||
|
|
||||||
|
let update_needed = (eula_needed && !eula_future)
|
||||||
|
|| (tos_needed && !tos_future)
|
||||||
|
|| (pp_needed && !pp_future);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
ui,
|
||||||
|
sender,
|
||||||
|
|
||||||
|
eula_needed,
|
||||||
|
tos_needed,
|
||||||
|
pp_needed,
|
||||||
|
|
||||||
|
eula_for_future,
|
||||||
|
tos_for_future,
|
||||||
|
pp_for_future,
|
||||||
|
|
||||||
|
eula_future,
|
||||||
|
tos_future,
|
||||||
|
pp_future,
|
||||||
|
|
||||||
|
update_needed,
|
||||||
|
|
||||||
|
eula: !eula_needed,
|
||||||
|
tos: !tos_needed,
|
||||||
|
pp: !pp_needed,
|
||||||
|
|
||||||
|
focus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Screen for TermsUpdaterScreen {
|
||||||
|
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, size: Rect) {
|
||||||
|
let mut needed_height = 5;
|
||||||
|
|
||||||
|
if size.height < 6 || size.width < 27 {
|
||||||
|
f.render_widget(
|
||||||
|
Line::from(format!("too small {}/63 by {}/15", size.width, size.height)),
|
||||||
|
size,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_width = 150;
|
||||||
|
let max_height = 30;
|
||||||
|
|
||||||
|
let content_width = if max_width < size.width {
|
||||||
|
max_width
|
||||||
|
} else {
|
||||||
|
size.width
|
||||||
|
};
|
||||||
|
let content_height = if max_height < size.height {
|
||||||
|
max_height
|
||||||
|
} else {
|
||||||
|
size.height
|
||||||
|
};
|
||||||
|
|
||||||
|
let horizontal_margin = (size.width.saturating_sub(content_width)) / 2;
|
||||||
|
let vertical_margin = (size.height.saturating_sub(content_height)) / 2;
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(7), Constraint::Length(3)])
|
||||||
|
.split(Rect {
|
||||||
|
x: horizontal_margin,
|
||||||
|
y: vertical_margin,
|
||||||
|
width: content_width,
|
||||||
|
height: content_height,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut text_lines: Vec<Line> = Vec::new();
|
||||||
|
let mut header_lines = 0;
|
||||||
|
let separator = if size.width > 72 {
|
||||||
|
header_lines += 3;
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"You previously accepted earlier versions of Tensamin’s legal documents.",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"Some of them have been updated and are listed below for your review.",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
header_lines += 5;
|
||||||
|
text_lines.push(Line::from("You previously accepted earlier "));
|
||||||
|
text_lines.push(Line::from("versions of Tensamin’s legal documents."));
|
||||||
|
text_lines.push(Line::from("Some of them have been updated and"));
|
||||||
|
text_lines.push(Line::from("are listed below for your review."));
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
4
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.eula_needed {
|
||||||
|
header_lines += 1;
|
||||||
|
if self.eula_future {
|
||||||
|
if size.width < 80 {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"EULA ¹³ (https://legal.tensamin.net/eula/newest/)",
|
||||||
|
self.eula,
|
||||||
|
self.focus == Focus::Eula,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
|
||||||
|
header_lines += 1;
|
||||||
|
let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64;
|
||||||
|
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||||
|
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||||
|
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||||
|
} else {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)",
|
||||||
|
self.eula,
|
||||||
|
self.focus == Focus::Eula,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
|
||||||
|
header_lines += 1;
|
||||||
|
let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64;
|
||||||
|
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||||
|
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||||
|
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if size.width < 80 {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"EULA ¹ (https://legal.tensamin.net/eula/newest/)",
|
||||||
|
self.eula,
|
||||||
|
self.focus == Focus::Eula,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)",
|
||||||
|
self.eula,
|
||||||
|
self.focus == Focus::Eula,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.tos_needed {
|
||||||
|
header_lines += 1;
|
||||||
|
if self.tos_future {
|
||||||
|
if size.width < 80 {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"ToS ²³ (https://legal.tensamin.net/tos/newest/)",
|
||||||
|
self.tos,
|
||||||
|
self.focus == Focus::Tos,
|
||||||
|
self.eula,
|
||||||
|
));
|
||||||
|
|
||||||
|
header_lines += 1;
|
||||||
|
let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64;
|
||||||
|
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||||
|
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||||
|
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||||
|
} else {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)",
|
||||||
|
self.tos,
|
||||||
|
self.focus == Focus::Tos,
|
||||||
|
self.eula,
|
||||||
|
));
|
||||||
|
|
||||||
|
header_lines += 1;
|
||||||
|
let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64;
|
||||||
|
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||||
|
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||||
|
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if size.width < 80 {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"ToS ² (https://legal.tensamin.net/tos/newest/)",
|
||||||
|
self.tos,
|
||||||
|
self.focus == Focus::Tos,
|
||||||
|
self.eula,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)",
|
||||||
|
self.tos,
|
||||||
|
self.focus == Focus::Tos,
|
||||||
|
self.eula,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.pp_needed {
|
||||||
|
header_lines += 1;
|
||||||
|
if self.pp_future {
|
||||||
|
if size.width < 80 {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)",
|
||||||
|
self.pp,
|
||||||
|
self.focus == Focus::Pp,
|
||||||
|
self.eula,
|
||||||
|
));
|
||||||
|
|
||||||
|
header_lines += 1;
|
||||||
|
let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64;
|
||||||
|
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||||
|
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||||
|
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||||
|
} else {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)",
|
||||||
|
self.pp,
|
||||||
|
self.focus == Focus::Pp,
|
||||||
|
self.eula,
|
||||||
|
));
|
||||||
|
|
||||||
|
header_lines += 1;
|
||||||
|
let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64;
|
||||||
|
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||||
|
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||||
|
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if size.width < 80 {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"PP ² (https://legal.tensamin.net/privacy-policy/newest/)",
|
||||||
|
self.pp,
|
||||||
|
self.focus == Focus::Pp,
|
||||||
|
self.eula,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
text_lines.push(checkbox(
|
||||||
|
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)",
|
||||||
|
self.pp,
|
||||||
|
self.focus == Focus::Pp,
|
||||||
|
self.eula,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
text_lines.push(Line::from("¹ Necessary to run the program"));
|
||||||
|
text_lines.push(Line::from("² Optional - only for Tensamin services"));
|
||||||
|
if size.width < 100 {
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"³ Future version - consent stored now, takes effect later",
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
text_lines.push(Line::from("³ Future version - You’ll continue using this version, automatically updated when changes apply."));
|
||||||
|
}
|
||||||
|
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
|
||||||
|
let mut optional_lines: Vec<i16> = if size.width > 143 {
|
||||||
|
if self.tos_needed || self.pp_needed {
|
||||||
|
text_lines.push(Line::from("By selecting Downgrade, you confirm that you agree to the End User License Agreement and applicable Terms of Service."));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"On Downgrade: Tensamin Services will deactivate once these changes apply.",
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
text_lines.push(Line::from("By selecting Continue, you confirm that you agree to the End User License Agreement and applicable Terms of Service."));
|
||||||
|
}
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link."));
|
||||||
|
|
||||||
|
if self.tos_needed || self.pp_needed {
|
||||||
|
vec![
|
||||||
|
header_lines + 7,
|
||||||
|
header_lines,
|
||||||
|
header_lines + 7,
|
||||||
|
separator,
|
||||||
|
header_lines + 2,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
header_lines + 6,
|
||||||
|
header_lines,
|
||||||
|
header_lines + 6,
|
||||||
|
separator,
|
||||||
|
header_lines + 2,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else if size.width > 92 {
|
||||||
|
if self.tos_needed || self.pp_needed {
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"By selecting Continue, you confirm that you agree to the End User",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"License Agreement and applicable Terms of Service.",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"On Downgrade: Tensamin Services will deactivate once these changes apply.",
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"By selecting Continue, you confirm that you agree to the End User",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"License Agreement and applicable Terms of Service.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link."));
|
||||||
|
|
||||||
|
if self.tos_needed || self.pp_needed {
|
||||||
|
vec![
|
||||||
|
header_lines + 8,
|
||||||
|
header_lines,
|
||||||
|
header_lines + 8,
|
||||||
|
separator,
|
||||||
|
header_lines + 2,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
header_lines + 7,
|
||||||
|
header_lines,
|
||||||
|
header_lines + 7,
|
||||||
|
separator,
|
||||||
|
header_lines + 2,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else if size.width > 73 {
|
||||||
|
if self.tos_needed || self.pp_needed {
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"By selecting Continue, you confirm that you read, understood and",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"agree to the End User License Agreement and applicable Terms of Service.",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"On Downgrade: Tensamin Services will deactivate once these changes apply.",
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"By selecting Continue, you confirm that you read, understood and",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"agree to the End User License Agreement and applicable Terms of Service.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"Tensamin services require acceptance of the ToS and Privacy Policy.",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"While having a document selected press O to view in this UI or press L",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from("to open as a link."));
|
||||||
|
|
||||||
|
if self.tos_needed || self.pp_needed {
|
||||||
|
vec![
|
||||||
|
header_lines + 8,
|
||||||
|
header_lines,
|
||||||
|
header_lines + 8,
|
||||||
|
separator,
|
||||||
|
header_lines + 2,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
header_lines + 7,
|
||||||
|
header_lines,
|
||||||
|
header_lines + 7,
|
||||||
|
separator,
|
||||||
|
header_lines + 2,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.tos_needed || self.pp_needed {
|
||||||
|
text_lines.push(Line::from("By selecting Continue, you confirm that you"));
|
||||||
|
text_lines.push(Line::from("agree to the End User License"));
|
||||||
|
text_lines.push(Line::from("Agreement and applicable Terms of Service."));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"On Downgrade: Tensamin Services will deactivate",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from("once these changes apply."));
|
||||||
|
} else {
|
||||||
|
text_lines.push(Line::from("By selecting Continue, you confirm that you"));
|
||||||
|
text_lines.push(Line::from("agree to the End User License"));
|
||||||
|
text_lines.push(Line::from("Agreement and applicable Terms of Service."));
|
||||||
|
}
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
text_lines.push(Line::from("Tensamin services require acceptance of the"));
|
||||||
|
text_lines.push(Line::from("Terms of Service and Privacy Policy."));
|
||||||
|
text_lines.push(Line::from(""));
|
||||||
|
text_lines.push(Line::from(
|
||||||
|
"While having a document selected press O to view",
|
||||||
|
));
|
||||||
|
text_lines.push(Line::from("in this UI or press L to open as a link."));
|
||||||
|
if self.tos_needed || self.pp_needed {
|
||||||
|
vec![
|
||||||
|
header_lines + 10,
|
||||||
|
header_lines,
|
||||||
|
header_lines + 11,
|
||||||
|
separator,
|
||||||
|
header_lines + 2,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
header_lines + 8,
|
||||||
|
header_lines,
|
||||||
|
header_lines + 9,
|
||||||
|
separator,
|
||||||
|
header_lines + 2,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
while size.height - 5 < text_lines.len() as u16 {
|
||||||
|
if optional_lines.len() == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
text_lines.remove(optional_lines[0] as usize);
|
||||||
|
optional_lines.remove(0);
|
||||||
|
}
|
||||||
|
needed_height += text_lines.len();
|
||||||
|
|
||||||
|
let q_informer = if self.update_needed {
|
||||||
|
"Q to Exit"
|
||||||
|
} else {
|
||||||
|
"Q to Cancel"
|
||||||
|
};
|
||||||
|
if size.width < 60 || size.height < needed_height as u16 {
|
||||||
|
let width_style = if size.width > 76 {
|
||||||
|
Style::default().fg(Color::Green)
|
||||||
|
} else if size.width >= 60 {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Red)
|
||||||
|
};
|
||||||
|
|
||||||
|
let height_style = if size.height > 20 {
|
||||||
|
Style::default().fg(Color::Green)
|
||||||
|
} else if size.height >= (header_lines as u16 + 10) {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Red)
|
||||||
|
};
|
||||||
|
|
||||||
|
let warning_text = Text::from(vec![
|
||||||
|
Line::from(vec![
|
||||||
|
Span::raw("Width: "),
|
||||||
|
Span::styled(format!("{}", size.width), width_style),
|
||||||
|
Span::raw(" / 60"),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::raw("Height: "),
|
||||||
|
Span::styled(format!("{}", size.height), height_style),
|
||||||
|
Span::raw(format!(" / {}", header_lines + 10)),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let warning = Paragraph::new(warning_text)
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(format!("UI Too Small [{}]", q_informer)),
|
||||||
|
);
|
||||||
|
|
||||||
|
f.render_widget(warning, size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let consent_block = Paragraph::new(Text::from(text_lines)).block(
|
||||||
|
Block::default()
|
||||||
|
.title(format!(" Update Tensamin User Consent [{}] ", q_informer))
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
);
|
||||||
|
f.render_widget(consent_block, chunks[0]);
|
||||||
|
|
||||||
|
let downgrade_scenario = self.tos_needed || self.pp_needed;
|
||||||
|
draw_buttons(
|
||||||
|
f,
|
||||||
|
chunks[1],
|
||||||
|
self.focus,
|
||||||
|
(self.eula, self.tos && self.pp),
|
||||||
|
self.update_needed,
|
||||||
|
downgrade_scenario,
|
||||||
|
self.pp_needed || self.tos_needed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
|
||||||
|
let mut possible_states = Vec::new();
|
||||||
|
|
||||||
|
if self.eula_needed {
|
||||||
|
possible_states.push(Focus::Eula);
|
||||||
|
}
|
||||||
|
if self.tos_needed {
|
||||||
|
possible_states.push(Focus::Tos);
|
||||||
|
}
|
||||||
|
if self.pp_needed {
|
||||||
|
possible_states.push(Focus::Pp);
|
||||||
|
}
|
||||||
|
|
||||||
|
possible_states.push(Focus::Cancel);
|
||||||
|
|
||||||
|
if self.eula {
|
||||||
|
possible_states.push(Focus::Continue);
|
||||||
|
if self.tos && self.pp {
|
||||||
|
possible_states.push(Focus::ContinueAll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match event.code {
|
||||||
|
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
|
||||||
|
if let Some(sender) = self.sender.take() {
|
||||||
|
let _ = sender.send(UserChoice::Deny);
|
||||||
|
}
|
||||||
|
return InteractionResult::CloseScreen;
|
||||||
|
}
|
||||||
|
KeyCode::Enter | KeyCode::Char(' ') => match self.focus {
|
||||||
|
Focus::Eula => {
|
||||||
|
self.eula = !self.eula;
|
||||||
|
self.tos = !self.tos_needed;
|
||||||
|
self.pp = !self.pp_needed;
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
Focus::Tos if self.eula => {
|
||||||
|
self.tos = !self.tos;
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
Focus::Pp if self.eula => {
|
||||||
|
self.pp = !self.pp;
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
Focus::Cancel => {
|
||||||
|
if let Some(sender) = self.sender.take() {
|
||||||
|
let _ = sender.send(UserChoice::Deny);
|
||||||
|
}
|
||||||
|
InteractionResult::CloseScreen
|
||||||
|
}
|
||||||
|
Focus::Continue if self.eula => {
|
||||||
|
if let Some(sender) = self.sender.take() {
|
||||||
|
let _ = sender.send(UserChoice::AcceptEULA);
|
||||||
|
}
|
||||||
|
InteractionResult::CloseScreen
|
||||||
|
}
|
||||||
|
Focus::ContinueAll if self.eula && self.tos && self.pp => {
|
||||||
|
if let Some(sender) = self.sender.take() {
|
||||||
|
let _ = sender.send(UserChoice::AcceptAll);
|
||||||
|
}
|
||||||
|
InteractionResult::CloseScreen
|
||||||
|
}
|
||||||
|
_ => InteractionResult::Unhandled,
|
||||||
|
},
|
||||||
|
KeyCode::Char('o') | KeyCode::Char('O') => {
|
||||||
|
let terms_type = match self.focus {
|
||||||
|
Focus::Eula => Some(Type::EULA),
|
||||||
|
Focus::Tos => Some(Type::TOS),
|
||||||
|
Focus::Pp => Some(Type::PP),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(terms_type) = terms_type {
|
||||||
|
let ui = self.ui.clone();
|
||||||
|
let fut = Box::pin(async move {
|
||||||
|
let content = get_terms(terms_type.clone()).await.unwrap();
|
||||||
|
Box::new(FileViewer::new(
|
||||||
|
ui.clone(),
|
||||||
|
terms_type.to_string(),
|
||||||
|
&content,
|
||||||
|
)) as Box<dyn Screen>
|
||||||
|
});
|
||||||
|
|
||||||
|
return InteractionResult::OpenFutureScreen { screen: fut };
|
||||||
|
} else {
|
||||||
|
InteractionResult::Unhandled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('l') | KeyCode::Char('L') => match self.focus {
|
||||||
|
Focus::Eula => {
|
||||||
|
let _ = open::that(get_link(Type::EULA));
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
Focus::Tos => {
|
||||||
|
let _ = open::that(get_link(Type::TOS));
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
Focus::Pp => {
|
||||||
|
let _ = open::that(get_link(Type::PP));
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
_ => InteractionResult::Unhandled,
|
||||||
|
},
|
||||||
|
KeyCode::Up | KeyCode::Left => {
|
||||||
|
self.focus.prev(&possible_states);
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
KeyCode::Down | KeyCode::Right | KeyCode::Tab => {
|
||||||
|
self.focus.next(&possible_states);
|
||||||
|
InteractionResult::Handled
|
||||||
|
}
|
||||||
|
_ => InteractionResult::Unhandled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
215
src/gui/ui.rs
215
src/gui/ui.rs
|
|
@ -1,197 +1,64 @@
|
||||||
use crossterm::{
|
|
||||||
cursor::MoveTo,
|
|
||||||
execute,
|
|
||||||
terminal::{Clear, ClearType, EnterAlternateScreen},
|
|
||||||
};
|
|
||||||
use ratatui::{
|
|
||||||
Terminal,
|
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
layout::Size,
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
io::{self, Error},
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
||||||
|
|
||||||
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen};
|
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen};
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
struct ResizableBackend<'a> {
|
use ratatui::{Terminal, backend::CrosstermBackend, init};
|
||||||
backend: CrosstermBackend<&'a mut Vec<u8>>,
|
use std::sync::{Arc, Mutex};
|
||||||
size: Size,
|
use tokio::sync::RwLock;
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ResizableBackend<'a> {
|
|
||||||
fn new(buf: &'a mut Vec<u8>, width: u16, height: u16) -> Self {
|
|
||||||
Self {
|
|
||||||
backend: CrosstermBackend::new(buf),
|
|
||||||
size: Size::new(width, height),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Backend for ResizableBackend<'a> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn draw<'b, I>(&mut self, content: I) -> io::Result<()>
|
|
||||||
where
|
|
||||||
I: Iterator<Item = (u16, u16, &'b ratatui::buffer::Cell)>,
|
|
||||||
{
|
|
||||||
self.backend.draw(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
|
||||||
self.backend.hide_cursor()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_cursor(&mut self) -> io::Result<()> {
|
|
||||||
self.backend.show_cursor()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
|
||||||
self.backend.get_cursor()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
|
||||||
self.backend.set_cursor(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear(&mut self) -> io::Result<()> {
|
|
||||||
self.backend.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_region(&mut self, region: ratatui::backend::ClearType) -> io::Result<()> {
|
|
||||||
self.backend.clear_region(region)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size(&self) -> Result<Size, Error> {
|
|
||||||
Ok(self.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
self.backend.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cursor_position(&mut self) -> Result<ratatui::prelude::Position, Self::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_cursor_position<P: Into<ratatui::prelude::Position>>(
|
|
||||||
&mut self,
|
|
||||||
_position: P,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_size(&mut self) -> Result<ratatui::prelude::backend::WindowSize, Self::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// UI state and rendering
|
/// UI state and rendering
|
||||||
|
|
||||||
pub struct UI {
|
pub struct UI {
|
||||||
pub cols: Arc<Mutex<u16>>,
|
pub terminal: Arc<Mutex<Terminal<CrosstermBackend<std::io::Stdout>>>>,
|
||||||
pub rows: Arc<Mutex<u16>>,
|
screen: Arc<RwLock<Option<Box<dyn Screen>>>>,
|
||||||
screen: Arc<Mutex<Option<Box<dyn Screen>>>>,
|
|
||||||
|
|
||||||
cached_render: Arc<Mutex<Vec<u8>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UI {
|
impl UI {
|
||||||
pub fn new(cols: u16, rows: u16) -> Self {
|
pub fn new() -> Self {
|
||||||
|
let terminal = init();
|
||||||
Self {
|
Self {
|
||||||
cols: Arc::new(Mutex::new(cols)),
|
terminal: Arc::new(Mutex::new(terminal)),
|
||||||
rows: Arc::new(Mutex::new(rows)),
|
screen: Arc::new(RwLock::new(None)),
|
||||||
screen: Arc::new(Mutex::new(None)),
|
|
||||||
cached_render: Arc::new(Mutex::new(Vec::new())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_screen(&self, screen: Box<dyn Screen>) {
|
pub async fn set_screen(&self, screen: Box<dyn Screen>) {
|
||||||
*self.screen.lock().unwrap() = Some(screen);
|
*self.screen.write().await = Some(screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&self, cols: u32, rows: u32) {
|
pub async fn handle_input(self: Arc<Self>, key_event: KeyEvent) {
|
||||||
*self.cols.lock().unwrap() = cols as u16;
|
let result = {
|
||||||
*self.rows.lock().unwrap() = rows as u16;
|
let mut guard = self.screen.write().await;
|
||||||
}
|
if let Some(screen) = guard.as_mut() {
|
||||||
|
screen.handle_input(key_event)
|
||||||
pub async fn handle_input(&self, input: &[u8]) {
|
|
||||||
let key_event = if input.len() == 1 {
|
|
||||||
let c = input[0] as char;
|
|
||||||
if c.is_ascii() {
|
|
||||||
Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
match result {
|
||||||
let event = match input {
|
InteractionResult::OpenScreen { screen } => {
|
||||||
b"\x1b[A" => Some(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)),
|
self.set_screen(screen).await;
|
||||||
b"\x1b[B" => Some(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)),
|
|
||||||
b"\x1b[C" => Some(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)),
|
|
||||||
b"\x1b[D" => Some(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)),
|
|
||||||
b"\r" | b"\n" => Some(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
|
|
||||||
b"\x7f" => Some(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)),
|
|
||||||
_ => key_event,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(event) = event {
|
|
||||||
let result = {
|
|
||||||
let mut guard = self.screen.lock().unwrap();
|
|
||||||
if let Some(screen) = guard.as_mut() {
|
|
||||||
screen.handle_input(event)
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match result {
|
|
||||||
InteractionResult::OpenScreen { screen } => {
|
|
||||||
self.set_screen(screen);
|
|
||||||
}
|
|
||||||
InteractionResult::OpenFutureScreen { screen: fut } => {
|
|
||||||
let ui = self.clone();
|
|
||||||
let screen = fut.await;
|
|
||||||
ui.set_screen(screen);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
InteractionResult::OpenFutureScreen { screen: fut } => {
|
||||||
|
let ui = self.clone();
|
||||||
|
let screen = fut.await;
|
||||||
|
ui.set_screen(screen).await;
|
||||||
|
}
|
||||||
|
InteractionResult::CloseScreen => {
|
||||||
|
/* TODO
|
||||||
|
self.set_screen();
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
InteractionResult::Handled => {}
|
||||||
|
InteractionResult::Unhandled => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&self) -> Vec<u8> {
|
pub async fn render(&self) {
|
||||||
let mut buf = Vec::new();
|
if let Some(screen) = self.screen.read().await.as_ref() {
|
||||||
|
let mut terminal = self.terminal.lock().unwrap();
|
||||||
execute!(
|
terminal
|
||||||
&mut buf,
|
.draw(|f| {
|
||||||
EnterAlternateScreen,
|
screen.render(f, f.area());
|
||||||
Clear(ClearType::All),
|
})
|
||||||
MoveTo(0, 0)
|
.unwrap();
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
{
|
|
||||||
let backend = ResizableBackend::new(
|
|
||||||
&mut buf,
|
|
||||||
*self.cols.lock().unwrap(),
|
|
||||||
*self.rows.lock().unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut terminal = Terminal::new(backend).unwrap();
|
|
||||||
|
|
||||||
if let Some(screen) = self.screen.lock().unwrap().as_ref() {
|
|
||||||
let _ = terminal.draw(|f| screen.render(f, f.area()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*self.cached_render.lock().unwrap() = buf.clone();
|
|
||||||
|
|
||||||
buf
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
src/main.rs
31
src/main.rs
|
|
@ -20,11 +20,12 @@ mod util;
|
||||||
use crate::communities::community_manager;
|
use crate::communities::community_manager;
|
||||||
use crate::communities::interactables::registry;
|
use crate::communities::interactables::registry;
|
||||||
use crate::gui::app_state::AppState;
|
use crate::gui::app_state::AppState;
|
||||||
use crate::gui::ui;
|
use crate::gui::input_handler::setup_input_handler;
|
||||||
|
use crate::gui::ui::UI;
|
||||||
use crate::langu::language_creator;
|
use crate::langu::language_creator;
|
||||||
use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection};
|
use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection};
|
||||||
use crate::server::server::start;
|
use crate::server::server::start;
|
||||||
use crate::terms::consent_state;
|
use crate::terms::consent_state::ConsentManager;
|
||||||
use crate::users::user_manager;
|
use crate::users::user_manager;
|
||||||
use crate::util::config_util::CONFIG;
|
use crate::util::config_util::CONFIG;
|
||||||
use crate::util::file_util::download_and_extract_zip;
|
use crate::util::file_util::download_and_extract_zip;
|
||||||
|
|
@ -45,17 +46,29 @@ async fn main() {
|
||||||
*RELOAD.write().await = false;
|
*RELOAD.write().await = false;
|
||||||
*SHUTDOWN.write().await = false;
|
*SHUTDOWN.write().await = false;
|
||||||
|
|
||||||
let tui = ui::UI::new(100, 100);
|
let ui = Arc::new(UI::new());
|
||||||
tokio::spawn(async move { tui.render() });
|
setup_input_handler(ui.clone());
|
||||||
|
|
||||||
// EULA
|
let ui_clone = ui.clone();
|
||||||
let (tos, pp) = consent_state::ConsentManager::check().await;
|
tokio::spawn(async move {
|
||||||
if !tos {
|
loop {
|
||||||
|
ui_clone.render().await;
|
||||||
|
sleep(Duration::from_millis(16)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let (eula, tos_pp) = ConsentManager::check(ui.clone()).await;
|
||||||
|
|
||||||
|
if !eula {
|
||||||
|
ui.terminal.lock().unwrap().clear();
|
||||||
|
ui.terminal.lock().unwrap().flush();
|
||||||
println!("You need to accept our End User Licence Agreement before launching!");
|
println!("You need to accept our End User Licence Agreement before launching!");
|
||||||
println!("You can find this at 'agreements'!");
|
println!("You can find this at 'agreements'!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if !pp {
|
if !tos_pp {
|
||||||
|
ui.terminal.lock().unwrap().clear();
|
||||||
|
ui.terminal.lock().unwrap().flush();
|
||||||
println!(
|
println!(
|
||||||
"Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!"
|
"Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!"
|
||||||
);
|
);
|
||||||
|
|
@ -171,5 +184,7 @@ async fn main() {
|
||||||
community_manager::clear();
|
community_manager::clear();
|
||||||
*APP_STATE.lock().unwrap() = AppState::new();
|
*APP_STATE.lock().unwrap() = AppState::new();
|
||||||
}
|
}
|
||||||
|
ui.terminal.lock().unwrap().clear();
|
||||||
|
ui.terminal.lock().unwrap().flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,137 @@
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
gui::{
|
||||||
|
screens::{terms_checker::TermsCheckerScreen, terms_updater::TermsUpdaterScreen},
|
||||||
|
ui::UI,
|
||||||
|
},
|
||||||
terms::{
|
terms::{
|
||||||
doc::Doc,
|
doc::Doc,
|
||||||
terms_checker,
|
|
||||||
terms_getter::{Type, get_current_docs, get_newest_docs},
|
terms_getter::{Type, get_current_docs, get_newest_docs},
|
||||||
terms_updater,
|
|
||||||
},
|
},
|
||||||
util::file_util::{load_file, save_file},
|
util::file_util::{load_file, save_file},
|
||||||
};
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
pub struct ConsentManager;
|
pub struct ConsentManager;
|
||||||
impl ConsentManager {
|
impl ConsentManager {
|
||||||
pub async fn check() -> (bool, bool) {
|
pub async fn check(ui: Arc<UI>) -> (bool, bool) {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
let file = load_file("", "agreements");
|
let file = load_file("", "agreements");
|
||||||
let (mut file_state, changed) = ConsentState::from_str(&file).sanitize();
|
let mut file_state = ConsentState::from_str(&file).sanitize();
|
||||||
|
ui.set_screen(Box::new(TermsCheckerScreen::new(ui.clone(), Some(tx))))
|
||||||
|
.await;
|
||||||
|
|
||||||
if changed {
|
let result = rx.await.unwrap_or(UserChoice::Deny);
|
||||||
save_file("", "agreements", &file_state.to_string());
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserChoice::Deny => {
|
||||||
|
return (false, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !file_state.accepted_eula {
|
save_file("", "agreements", &file_state.to_string());
|
||||||
file_state = terms_checker::run_consent_ui(file_state).await;
|
|
||||||
|
|
||||||
save_file("", "agreements", &file_state.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !file_state.accepted_eula {
|
|
||||||
return (false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((eula_update, tos_update, privacy_update)) = Self::get_updates().await {
|
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!(tos_update, UpdateDecision::Forced(_))
|
||||||
|| matches!(privacy_update, UpdateDecision::Forced(_));
|
|| matches!(privacy_update, UpdateDecision::Forced(_));
|
||||||
|
|
||||||
let updater_logic = async move {
|
if let Some((eula_update, tos_update, privacy_update)) = Self::get_updates().await {
|
||||||
let (state_to_update, _) = terms_updater::run_consent_ui(
|
let is_forced = matches!(eula_update, UpdateDecision::Forced(_))
|
||||||
file_state,
|
|| matches!(tos_update, UpdateDecision::Forced(_))
|
||||||
eula_update,
|
|| matches!(privacy_update, UpdateDecision::Forced(_));
|
||||||
tos_update,
|
|
||||||
privacy_update,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.sanitize();
|
|
||||||
|
|
||||||
save_file("", "agreements", &state_to_update.to_string());
|
let (tx, rx) = oneshot::channel();
|
||||||
};
|
|
||||||
// ======================================================== //
|
ui.set_screen(Box::new(TermsUpdaterScreen::new(
|
||||||
// TODO: Implement UI Drawing order, draw update As PoPup //
|
ui.clone(),
|
||||||
// ======================================================== //
|
eula_update.clone(),
|
||||||
//if is_forced {
|
tos_update.clone(),
|
||||||
updater_logic.await;
|
privacy_update.clone(),
|
||||||
//} else {
|
Some(tx),
|
||||||
//task::spawn(updater_logic);
|
)))
|
||||||
//}
|
.await;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
UserChoice::AcceptEULA => {
|
||||||
|
state.accepted_eula = true;
|
||||||
|
}
|
||||||
|
UserChoice::Deny => {
|
||||||
|
return (false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = state.sanitize();
|
||||||
|
save_file("", "agreements", &state.to_string());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let file = load_file("", "agreements");
|
let file = load_file("", "agreements");
|
||||||
let (final_state, changed) = ConsentState::from_str(&file).sanitize();
|
let final_state = ConsentState::from_str(&file).sanitize();
|
||||||
if changed {
|
|
||||||
save_file("", "agreements", &final_state.to_string());
|
save_file("", "agreements", &final_state.to_string());
|
||||||
}
|
|
||||||
(
|
(
|
||||||
final_state.accepted_eula,
|
final_state.accepted_eula,
|
||||||
final_state.accepted_tos && final_state.accepted_pp,
|
final_state.accepted_tos && final_state.accepted_pp,
|
||||||
|
|
@ -76,10 +147,9 @@ impl ConsentManager {
|
||||||
UpdateDecision,
|
UpdateDecision,
|
||||||
)> {
|
)> {
|
||||||
let file = load_file("", "agreements");
|
let file = load_file("", "agreements");
|
||||||
let (accepted_state, changed) = ConsentState::from_str(&file).sanitize();
|
let accepted_state = ConsentState::from_str(&file).sanitize();
|
||||||
if changed {
|
save_file("", "agreements", &accepted_state.to_string());
|
||||||
save_file("", "agreements", &accepted_state.to_string());
|
|
||||||
}
|
|
||||||
if let (
|
if let (
|
||||||
Some((current_eula, current_tos, current_privacy)),
|
Some((current_eula, current_tos, current_privacy)),
|
||||||
Some((newest_eula, newest_tos, newest_privacy)),
|
Some((newest_eula, newest_tos, newest_privacy)),
|
||||||
|
|
@ -186,8 +256,7 @@ pub struct ConsentState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConsentState {
|
impl ConsentState {
|
||||||
fn sanitize(mut self) -> (Self, bool) {
|
fn sanitize(mut self) -> Self {
|
||||||
let mut changed = false;
|
|
||||||
let current_secs = SystemTime::now()
|
let current_secs = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
@ -197,30 +266,26 @@ impl ConsentState {
|
||||||
if future_eula.get_time() < current_secs {
|
if future_eula.get_time() < current_secs {
|
||||||
self.eula = Some(future_eula);
|
self.eula = Some(future_eula);
|
||||||
self.future_eula = None;
|
self.future_eula = None;
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(future_tos) = self.future_tos.clone() {
|
if let Some(future_tos) = self.future_tos.clone() {
|
||||||
if future_tos.get_time() < current_secs {
|
if future_tos.get_time() < current_secs {
|
||||||
self.tos = Some(future_tos);
|
self.tos = Some(future_tos);
|
||||||
self.future_tos = None;
|
self.future_tos = None;
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(future_privacy) = self.future_privacy.clone() {
|
if let Some(future_privacy) = self.future_privacy.clone() {
|
||||||
if future_privacy.get_time() < current_secs {
|
if future_privacy.get_time() < current_secs {
|
||||||
self.privacy = Some(future_privacy);
|
self.privacy = Some(future_privacy);
|
||||||
self.future_privacy = None;
|
self.future_privacy = None;
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.accepted_eula {
|
if !self.accepted_eula {
|
||||||
self.accepted_tos = false;
|
self.accepted_tos = false;
|
||||||
self.accepted_pp = false;
|
self.accepted_pp = false;
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
(self, changed)
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_string(&self) -> String {
|
pub fn to_string(&self) -> String {
|
||||||
|
|
@ -356,7 +421,7 @@ impl ConsentState {
|
||||||
pp_version = v.to_string();
|
pp_version = v.to_string();
|
||||||
} else if let Some(v) = line.strip_prefix("Privacy-Policy-HASH=") {
|
} else if let Some(v) = line.strip_prefix("Privacy-Policy-HASH=") {
|
||||||
pp_hash = v.to_string();
|
pp_hash = v.to_string();
|
||||||
} else if let Some(v) = line.strip_prefix("UNIX=") {
|
} else if let Some(v) = line.strip_prefix("UNIX-SECOND=") {
|
||||||
unix = v.to_string();
|
unix = v.to_string();
|
||||||
} else if let Some(v) = line.strip_prefix("FUTURE-EULA-VERSION=") {
|
} else if let Some(v) = line.strip_prefix("FUTURE-EULA-VERSION=") {
|
||||||
future_eula_version = v.to_string();
|
future_eula_version = v.to_string();
|
||||||
|
|
@ -382,7 +447,7 @@ impl ConsentState {
|
||||||
let future_eula_time = future_eula_time.parse::<u64>().unwrap_or(0);
|
let future_eula_time = future_eula_time.parse::<u64>().unwrap_or(0);
|
||||||
let future_tos_time = future_tos_time.parse::<u64>().unwrap_or(0);
|
let future_tos_time = future_tos_time.parse::<u64>().unwrap_or(0);
|
||||||
let future_pp_time = future_pp_time.parse::<u64>().unwrap_or(0);
|
let future_pp_time = future_pp_time.parse::<u64>().unwrap_or(0);
|
||||||
let (state, _) = Self {
|
let state = Self {
|
||||||
accepted_eula: eula,
|
accepted_eula: eula,
|
||||||
eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)),
|
eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)),
|
||||||
accepted_pp: pp,
|
accepted_pp: pp,
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,62 @@
|
||||||
use crossterm::event::{self, Event, KeyCode};
|
use crossterm::event::{self, Event, KeyCode, KeyEvent};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
DefaultTerminal,
|
DefaultTerminal,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Paragraph, Wrap},
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
};
|
};
|
||||||
use std::time::Duration;
|
use std::{any::Any, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen, ui::UI};
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct FileViewer {
|
pub struct FileViewer {
|
||||||
|
ui: Arc<UI>,
|
||||||
title: String,
|
title: String,
|
||||||
text: Vec<DisplayLine>,
|
text: Vec<DisplayLine>,
|
||||||
scroll: u16,
|
scroll: u16,
|
||||||
scroll_x: u16,
|
scroll_x: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Screen for FileViewer {
|
||||||
|
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) {
|
||||||
|
self.draw(f, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
|
||||||
|
match event.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => {
|
||||||
|
return InteractionResult::CloseScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyCode::Down => self.scroll = self.scroll.saturating_add(1),
|
||||||
|
KeyCode::Up => self.scroll = self.scroll.saturating_sub(1),
|
||||||
|
KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10),
|
||||||
|
KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10),
|
||||||
|
KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2),
|
||||||
|
KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2),
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
InteractionResult::Unhandled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FileViewer {
|
impl FileViewer {
|
||||||
pub fn new(title: String, content: &str) -> Self {
|
pub fn new(ui: Arc<UI>, title: String, content: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
ui,
|
||||||
title,
|
title,
|
||||||
text: parse_document(content.to_owned()),
|
text: parse_document(content.to_owned()),
|
||||||
scroll: 0,
|
scroll: 0,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,4 @@ pub mod consent_state;
|
||||||
pub mod doc;
|
pub mod doc;
|
||||||
pub mod focus;
|
pub mod focus;
|
||||||
pub mod md_viewer;
|
pub mod md_viewer;
|
||||||
pub mod terms_checker;
|
|
||||||
pub mod terms_getter;
|
pub mod terms_getter;
|
||||||
pub mod terms_updater;
|
|
||||||
|
|
|
||||||
|
|
@ -1,311 +0,0 @@
|
||||||
use crate::terms::{
|
|
||||||
buttons::{checkbox, draw_buttons},
|
|
||||||
consent_state::{ConsentState, UserChoice},
|
|
||||||
focus::Focus,
|
|
||||||
md_viewer::FileViewer,
|
|
||||||
terms_getter::{Type, get_current_docs, get_link, get_terms},
|
|
||||||
};
|
|
||||||
use crossterm::event::{self, Event, KeyCode};
|
|
||||||
use ratatui::{
|
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
|
||||||
style::{Color, Style},
|
|
||||||
text::{Line, Span, Text},
|
|
||||||
widgets::{Block, Borders, Paragraph},
|
|
||||||
};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|
||||||
let mut terminal = ratatui::init();
|
|
||||||
|
|
||||||
let (mut eula, mut tos, mut pp) = (false, false, false);
|
|
||||||
let mut focus = Focus::Eula;
|
|
||||||
|
|
||||||
let result = loop {
|
|
||||||
let mut too_small = false;
|
|
||||||
terminal
|
|
||||||
.draw(|f| {
|
|
||||||
let mut needed_height = 5;
|
|
||||||
|
|
||||||
let size = f.area();
|
|
||||||
|
|
||||||
if size.height < 6
|
|
||||||
|| size.width < 27 {
|
|
||||||
f.render_widget(Line::from(format!("too small {}/63 by {}/13", size.width, size.height)), size);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_width = 150;
|
|
||||||
let max_height = 26;
|
|
||||||
|
|
||||||
let content_width = if max_width < size.width { max_width } else { size.width} ;
|
|
||||||
let content_height = if max_height < size.height { max_height } else { size.height} ;
|
|
||||||
|
|
||||||
let horizontal_margin = (size.width.saturating_sub(content_width)) / 2;
|
|
||||||
let vertical_margin = (size.height.saturating_sub(content_height)) / 2;
|
|
||||||
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Min(7), Constraint::Length(3)])
|
|
||||||
.split(Rect {
|
|
||||||
x: horizontal_margin,
|
|
||||||
y: vertical_margin,
|
|
||||||
width: content_width,
|
|
||||||
height: content_height,
|
|
||||||
});
|
|
||||||
let eula_text = if size.width < 70 {
|
|
||||||
"EULA ¹ (https://legal.tensamin.net/eula/)"
|
|
||||||
} else {
|
|
||||||
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)"
|
|
||||||
};
|
|
||||||
let tos_text = if size.width < 72 {
|
|
||||||
"ToS ² (https://legal.tensamin.net/terms-of-service/)"
|
|
||||||
} else {
|
|
||||||
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)"
|
|
||||||
};
|
|
||||||
let pp_text = if size.width < 68 {
|
|
||||||
"PP ² (https://legal.tensamin.net/privacy-policy/)"
|
|
||||||
} else {
|
|
||||||
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)"
|
|
||||||
};
|
|
||||||
|
|
||||||
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) =
|
|
||||||
if size.width > 143 {
|
|
||||||
(
|
|
||||||
vec![8, 3, 8, 5],
|
|
||||||
vec![
|
|
||||||
"",
|
|
||||||
"By selecting Continue, you confirm that you agree to the End User License Agreement and applicable Terms of Service.",
|
|
||||||
"",
|
|
||||||
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
|
|
||||||
"",
|
|
||||||
"While having a document selected press O to view in this UI or press L to open as a link.",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
} else if size.width > 92 {
|
|
||||||
(
|
|
||||||
vec![9, 3, 9, 5],
|
|
||||||
vec![
|
|
||||||
"",
|
|
||||||
"By selecting Continue, you confirm that you agree to the End User License",
|
|
||||||
"Agreement and applicable Terms of Service.",
|
|
||||||
"",
|
|
||||||
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
|
|
||||||
"",
|
|
||||||
"While having a document selected press O to view in this UI or press L to open as a link.",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
} else if size.width > 73 {
|
|
||||||
(
|
|
||||||
vec![9, 3, 9, 5],
|
|
||||||
vec![
|
|
||||||
"",
|
|
||||||
"By selecting Continue, you confirm that you agree to the",
|
|
||||||
"End User License Agreement and applicable Terms of Service.",
|
|
||||||
"",
|
|
||||||
"Tensamin services require acceptance of the ToS and Privacy Policy.",
|
|
||||||
"",
|
|
||||||
"While having a document selected press O to view in this UI or press L",
|
|
||||||
"to open as a link.",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
vec![10, 3, 11, 5],
|
|
||||||
vec![
|
|
||||||
"",
|
|
||||||
"By selecting Continue, you confirm that you agree",
|
|
||||||
"to the End User License Agreement and",
|
|
||||||
"applicable Terms of Service.",
|
|
||||||
"",
|
|
||||||
"Tensamin services require acceptance of the",
|
|
||||||
"Terms of Service and Privacy Policy.",
|
|
||||||
"",
|
|
||||||
"While having a document selected press O to view",
|
|
||||||
"in this UI or press L to open as a link.",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let mut text_lines = vec![
|
|
||||||
checkbox(eula_text, eula, focus == Focus::Eula, true),
|
|
||||||
checkbox(tos_text, tos, focus == Focus::Tos, eula),
|
|
||||||
checkbox(pp_text, pp, focus == Focus::Pp, eula),
|
|
||||||
Line::from(""),
|
|
||||||
Line::from("¹ Necessary– required to run the program"),
|
|
||||||
Line::from("² Optional – required only for Tensamin services")
|
|
||||||
];
|
|
||||||
for line in agree_lines {
|
|
||||||
text_lines.insert(text_lines.len(), Line::from(line));
|
|
||||||
}
|
|
||||||
|
|
||||||
while size.height - 5 < text_lines.len() as u16 {
|
|
||||||
if optional_lines.len() == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
text_lines.remove(optional_lines[0] as usize);
|
|
||||||
optional_lines.remove(0);
|
|
||||||
}
|
|
||||||
needed_height += text_lines.len();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if size.width < 60 || size.height < needed_height as u16 {
|
|
||||||
let width_style = if size.width > 76 {
|
|
||||||
Style::default().fg(Color::Green)
|
|
||||||
} else if size.width >= 60 {
|
|
||||||
Style::default().fg(Color::Yellow)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::Red)
|
|
||||||
};
|
|
||||||
|
|
||||||
let height_style = if size.height > 19 {
|
|
||||||
Style::default().fg(Color::Green)
|
|
||||||
} else if size.height >= 13 {
|
|
||||||
Style::default().fg(Color::Yellow)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::Red)
|
|
||||||
};
|
|
||||||
|
|
||||||
let warning_text = Text::from(vec![
|
|
||||||
Line::from(vec![
|
|
||||||
Span::raw("Width: "),
|
|
||||||
Span::styled(format!("{}", size.width), width_style),
|
|
||||||
Span::raw(" / 60"),
|
|
||||||
]),
|
|
||||||
Line::from(vec![
|
|
||||||
Span::raw("Height: "),
|
|
||||||
Span::styled(format!("{}", size.height), height_style),
|
|
||||||
Span::raw(format!(" / 13")),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let warning = Paragraph::new(warning_text)
|
|
||||||
.alignment(Alignment::Center)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.title(format!("UI Too Small [Q to Quit]")),
|
|
||||||
);
|
|
||||||
|
|
||||||
too_small = true;
|
|
||||||
f.render_widget(warning, size);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let consent_block = Paragraph::new(Text::from(text_lines))
|
|
||||||
.block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL));
|
|
||||||
f.render_widget(consent_block, chunks[0]);
|
|
||||||
|
|
||||||
draw_buttons(f, chunks[1], focus, (eula, tos && pp), true, false, true);
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if event::poll(Duration::from_millis(200)).unwrap() {
|
|
||||||
if let Event::Key(key) = event::read().unwrap() {
|
|
||||||
if too_small {
|
|
||||||
if matches!(key.code, KeyCode::Char('q') | KeyCode::Char('Q')) {
|
|
||||||
break UserChoice::Deny;
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel];
|
|
||||||
if eula {
|
|
||||||
possible_states.push(Focus::Continue);
|
|
||||||
if tos && pp {
|
|
||||||
possible_states.push(Focus::ContinueAll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Esc => break UserChoice::Deny,
|
|
||||||
KeyCode::Up | KeyCode::Left => focus.prev(&possible_states),
|
|
||||||
KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next(&possible_states),
|
|
||||||
KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny,
|
|
||||||
KeyCode::Char('o') | KeyCode::Char('O') => {
|
|
||||||
let terms_type = match focus {
|
|
||||||
Focus::Eula => Type::EULA,
|
|
||||||
Focus::Tos => Type::TOS,
|
|
||||||
Focus::Pp => Type::PP,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(eula) = get_terms(terms_type.clone()).await {
|
|
||||||
terminal = FileViewer::new(terms_type.to_string(), &eula)
|
|
||||||
.force_popup(terminal);
|
|
||||||
} else {
|
|
||||||
terminal = FileViewer::new(
|
|
||||||
terms_type.to_string(),
|
|
||||||
"### A loading error occured\
|
|
||||||
\nTry reloading this site or retry in a moment.",
|
|
||||||
)
|
|
||||||
.force_popup(terminal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char('l') | KeyCode::Char('L') => {
|
|
||||||
let terms_type = match focus {
|
|
||||||
Focus::Eula => Type::EULA,
|
|
||||||
Focus::Tos => Type::TOS,
|
|
||||||
Focus::Pp => Type::PP,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = open::that(get_link(terms_type));
|
|
||||||
}
|
|
||||||
KeyCode::Char(' ') => match focus {
|
|
||||||
Focus::Eula => {
|
|
||||||
eula = !eula;
|
|
||||||
tos = false;
|
|
||||||
pp = false;
|
|
||||||
}
|
|
||||||
Focus::Tos => {
|
|
||||||
if eula {
|
|
||||||
tos = !tos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Focus::Pp => {
|
|
||||||
if eula {
|
|
||||||
pp = !pp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
KeyCode::Enter => match focus {
|
|
||||||
Focus::Cancel => break UserChoice::Deny,
|
|
||||||
Focus::Continue if eula => break UserChoice::AcceptEULA,
|
|
||||||
Focus::ContinueAll if eula && tos && pp => {
|
|
||||||
break UserChoice::AcceptAll;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ratatui::restore();
|
|
||||||
|
|
||||||
if let Some((eula, tos, privacy)) = get_current_docs().await {
|
|
||||||
consent.eula = Some(eula);
|
|
||||||
consent.tos = Some(tos);
|
|
||||||
consent.privacy = Some(privacy);
|
|
||||||
}
|
|
||||||
|
|
||||||
match result {
|
|
||||||
UserChoice::AcceptAll => {
|
|
||||||
consent.accepted_eula = true;
|
|
||||||
consent.accepted_tos = true;
|
|
||||||
consent.accepted_pp = true;
|
|
||||||
}
|
|
||||||
UserChoice::AcceptEULA => {
|
|
||||||
consent.accepted_eula = true;
|
|
||||||
consent.accepted_tos = false;
|
|
||||||
consent.accepted_pp = false;
|
|
||||||
}
|
|
||||||
UserChoice::Deny => {
|
|
||||||
consent.accepted_eula = false;
|
|
||||||
consent.accepted_tos = false;
|
|
||||||
consent.accepted_pp = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
consent
|
|
||||||
}
|
|
||||||
|
|
@ -1,480 +0,0 @@
|
||||||
use crate::terms::{
|
|
||||||
buttons::{checkbox, draw_buttons},
|
|
||||||
consent_state::{ConsentState, UpdateDecision, UserChoice},
|
|
||||||
doc::Doc,
|
|
||||||
focus::Focus,
|
|
||||||
md_viewer::FileViewer,
|
|
||||||
terms_getter::{Type, get_newest_link, get_terms},
|
|
||||||
};
|
|
||||||
use chrono::{Local, TimeZone, Utc};
|
|
||||||
use crossterm::event::{self, Event, KeyCode};
|
|
||||||
use ratatui::{
|
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
|
||||||
style::{Color, Style},
|
|
||||||
text::{Line, Span, Text},
|
|
||||||
widgets::{Block, Borders, Paragraph},
|
|
||||||
};
|
|
||||||
use std::time::Duration;
|
|
||||||
pub async fn run_consent_ui(
|
|
||||||
mut consent: ConsentState,
|
|
||||||
consent_eula: UpdateDecision,
|
|
||||||
consent_tos: UpdateDecision,
|
|
||||||
consent_pp: UpdateDecision,
|
|
||||||
) -> ConsentState {
|
|
||||||
let mut terminal = ratatui::init();
|
|
||||||
|
|
||||||
let (eula_needed, eula_future, eula_for_future): (bool, bool, Option<Doc>) = match consent_eula
|
|
||||||
{
|
|
||||||
UpdateDecision::NoChange => (false, false, None),
|
|
||||||
UpdateDecision::Future { newest } => (true, true, Some(newest)),
|
|
||||||
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
|
|
||||||
};
|
|
||||||
let (tos_needed, tos_future, tos_for_future): (bool, bool, Option<Doc>) = match consent_tos {
|
|
||||||
UpdateDecision::NoChange => (false, false, None),
|
|
||||||
UpdateDecision::Future { newest } => (true, true, Some(newest)),
|
|
||||||
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
|
|
||||||
};
|
|
||||||
let (pp_needed, pp_future, pp_for_future): (bool, bool, Option<Doc>) = match consent_pp {
|
|
||||||
UpdateDecision::NoChange => (false, false, None),
|
|
||||||
UpdateDecision::Future { newest } => (true, true, Some(newest)),
|
|
||||||
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
|
|
||||||
};
|
|
||||||
let update_needed =
|
|
||||||
(eula_needed && !eula_future) || (tos_needed && !tos_future) || (pp_needed && !pp_future);
|
|
||||||
let (mut eula, mut tos, mut pp) = (!eula_needed, !tos_needed, !pp_needed);
|
|
||||||
let mut focus = if eula_needed {
|
|
||||||
Focus::Eula
|
|
||||||
} else if tos_needed {
|
|
||||||
Focus::Tos
|
|
||||||
} else if pp_needed {
|
|
||||||
Focus::Pp
|
|
||||||
} else {
|
|
||||||
Focus::Cancel
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = loop {
|
|
||||||
let mut too_small = false;
|
|
||||||
terminal
|
|
||||||
.draw(|f| {
|
|
||||||
let mut needed_height = 5;
|
|
||||||
|
|
||||||
let size = f.area();
|
|
||||||
|
|
||||||
if size.height < 6
|
|
||||||
|| size.width < 27 {
|
|
||||||
f.render_widget(Line::from(format!("too small {}/63 by {}/15", size.width, size.height)), size);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_width = 150;
|
|
||||||
let max_height = 30;
|
|
||||||
|
|
||||||
let content_width = if max_width < size.width { max_width } else { size.width} ;
|
|
||||||
let content_height = if max_height < size.height { max_height } else { size.height} ;
|
|
||||||
|
|
||||||
let horizontal_margin = (size.width.saturating_sub(content_width)) / 2;
|
|
||||||
let vertical_margin = (size.height.saturating_sub(content_height)) / 2;
|
|
||||||
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Min(7), Constraint::Length(3)])
|
|
||||||
.split(Rect {
|
|
||||||
x: horizontal_margin,
|
|
||||||
y: vertical_margin,
|
|
||||||
width: content_width,
|
|
||||||
height: content_height,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
let mut text_lines: Vec<Line> = Vec::new();
|
|
||||||
let mut header_lines = 0;
|
|
||||||
let seperator = if size.width > 72 {
|
|
||||||
header_lines += 3;
|
|
||||||
text_lines.push(Line::from("You previously accepted earlier versions of Tensamin’s legal documents."));
|
|
||||||
text_lines.push(Line::from("Some of them have been updated and are listed below for your review."));
|
|
||||||
text_lines.push(Line::from(""));
|
|
||||||
2
|
|
||||||
} else {
|
|
||||||
header_lines += 5;
|
|
||||||
text_lines.push(Line::from("You previously accepted earlier "));
|
|
||||||
text_lines.push(Line::from("versions of Tensamin’s legal documents."));
|
|
||||||
text_lines.push(Line::from("Some of them have been updated and"));
|
|
||||||
text_lines.push(Line::from("are listed below for your review."));
|
|
||||||
text_lines.push(Line::from(""));
|
|
||||||
4
|
|
||||||
};
|
|
||||||
|
|
||||||
if eula_needed {
|
|
||||||
header_lines += 1;
|
|
||||||
if eula_future {
|
|
||||||
if size.width < 80 {
|
|
||||||
text_lines.push(checkbox("EULA ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
|
|
||||||
|
|
||||||
header_lines += 1;
|
|
||||||
let unix_timestamp = eula_for_future.clone().unwrap().get_time() as i64;
|
|
||||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
|
||||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
|
||||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
|
||||||
} else {
|
|
||||||
text_lines.push(checkbox("End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
|
|
||||||
|
|
||||||
header_lines += 1;
|
|
||||||
let unix_timestamp = eula_for_future.clone().unwrap().get_time() as i64;
|
|
||||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
|
||||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
|
||||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if size.width < 80 {
|
|
||||||
text_lines.push(checkbox("EULA ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
|
|
||||||
} else {
|
|
||||||
text_lines.push(checkbox("End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tos_needed {
|
|
||||||
header_lines += 1;
|
|
||||||
if tos_future {
|
|
||||||
if size.width < 80 {
|
|
||||||
text_lines.push(checkbox("ToS ²³ (https://legal.tensamin.net/tos/newest/)", tos, focus == Focus::Tos, eula));
|
|
||||||
|
|
||||||
header_lines += 1;
|
|
||||||
let unix_timestamp = tos_for_future.clone().unwrap().get_time() as i64;
|
|
||||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
|
||||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
|
||||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
|
||||||
} else {
|
|
||||||
text_lines.push(checkbox("Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
|
|
||||||
|
|
||||||
header_lines += 1;
|
|
||||||
let unix_timestamp = tos_for_future.clone().unwrap().get_time() as i64;
|
|
||||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
|
||||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
|
||||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if size.width < 80 {
|
|
||||||
text_lines.push(checkbox("ToS ² (https://legal.tensamin.net/tos/newest/)", tos, focus == Focus::Tos, eula));
|
|
||||||
} else {
|
|
||||||
text_lines.push(checkbox("Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pp_needed {
|
|
||||||
header_lines += 1;
|
|
||||||
if pp_future {
|
|
||||||
if size.width < 80 {
|
|
||||||
text_lines.push(checkbox("PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
|
|
||||||
|
|
||||||
header_lines += 1;
|
|
||||||
let unix_timestamp = pp_for_future.clone().unwrap().get_time() as i64;
|
|
||||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
|
||||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
|
||||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
|
||||||
} else {
|
|
||||||
text_lines.push(checkbox("Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
|
|
||||||
|
|
||||||
header_lines += 1;
|
|
||||||
let unix_timestamp = pp_for_future.clone().unwrap().get_time() as i64;
|
|
||||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
|
||||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
|
||||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if size.width < 80 {
|
|
||||||
text_lines.push(checkbox("PP ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
|
|
||||||
} else {
|
|
||||||
text_lines.push(checkbox("Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
text_lines.push(Line::from(""));
|
|
||||||
text_lines.push(Line::from("¹ Necessary to run the program"));
|
|
||||||
text_lines.push(Line::from("² Optional - only for Tensamin services"));
|
|
||||||
if size.width < 100 {
|
|
||||||
text_lines.push(Line::from("³ Future version - consent stored now, takes effect later"));
|
|
||||||
} else {
|
|
||||||
text_lines.push(Line::from("³ Future version - You’ll continue using this version, automatically updated when changes apply."));
|
|
||||||
}
|
|
||||||
|
|
||||||
text_lines.push(Line::from(""));
|
|
||||||
|
|
||||||
let mut optional_lines: Vec<i16> =
|
|
||||||
if size.width > 143 {
|
|
||||||
if tos_needed || pp_needed {
|
|
||||||
text_lines.push(Line::from("By selecting Downgrade, you confirm that you agree to the End User License Agreement and applicable Terms of Service."));
|
|
||||||
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate once these changes apply."));
|
|
||||||
} else {
|
|
||||||
text_lines.push(Line::from("By selecting Continue, you confirm that you agree to the End User License Agreement and applicable Terms of Service."));
|
|
||||||
}
|
|
||||||
text_lines.push(Line::from(""));
|
|
||||||
text_lines.push(Line::from("Tensamin services require acceptance of the Terms of Service and Privacy Policy."));
|
|
||||||
text_lines.push(Line::from(""));
|
|
||||||
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link."));
|
|
||||||
|
|
||||||
if tos_needed || pp_needed {
|
|
||||||
vec![header_lines + 7, header_lines, header_lines + 7, seperator, header_lines + 2]
|
|
||||||
} else {
|
|
||||||
vec![header_lines + 6, header_lines, header_lines + 6, seperator, header_lines + 2]
|
|
||||||
}
|
|
||||||
} else if size.width > 92 {
|
|
||||||
if tos_needed || pp_needed {
|
|
||||||
text_lines.push(Line::from("By selecting Continue, you confirm that you agree to the End User"));
|
|
||||||
text_lines.push(Line::from("License Agreement and applicable Terms of Service."));
|
|
||||||
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate once these changes apply."));
|
|
||||||
} else {
|
|
||||||
text_lines.push(Line::from("By selecting Continue, you confirm that you agree to the End User"));
|
|
||||||
text_lines.push(Line::from("License Agreement and applicable Terms of Service."));
|
|
||||||
}
|
|
||||||
|
|
||||||
text_lines.push(Line::from(""));
|
|
||||||
text_lines.push(Line::from("Tensamin services require acceptance of the Terms of Service and Privacy Policy."));
|
|
||||||
text_lines.push(Line::from(""));
|
|
||||||
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link."));
|
|
||||||
|
|
||||||
if tos_needed || pp_needed {
|
|
||||||
vec![header_lines + 8, header_lines, header_lines + 8, seperator, header_lines + 2]
|
|
||||||
} else {
|
|
||||||
vec![header_lines + 7, header_lines, header_lines + 7, seperator, header_lines + 2]
|
|
||||||
}
|
|
||||||
} else if size.width > 73 {
|
|
||||||
if tos_needed || pp_needed {
|
|
||||||
text_lines.push(Line::from("By selecting Continue, you confirm that you read, understood and"));
|
|
||||||
text_lines.push(Line::from("agree to the End User License Agreement and applicable Terms of Service."));
|
|
||||||
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate once these changes apply."));
|
|
||||||
} else {
|
|
||||||
text_lines.push(Line::from("By selecting Continue, you confirm that you read, understood and"));
|
|
||||||
text_lines.push(Line::from("agree to the End User License Agreement and applicable Terms of Service."));
|
|
||||||
}
|
|
||||||
text_lines.push(Line::from("",));
|
|
||||||
text_lines.push(Line::from("Tensamin services require acceptance of the ToS and Privacy Policy.",));
|
|
||||||
text_lines.push(Line::from("",));
|
|
||||||
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L",));
|
|
||||||
text_lines.push(Line::from("to open as a link.",));
|
|
||||||
|
|
||||||
if tos_needed || pp_needed {
|
|
||||||
vec![header_lines + 8, header_lines, header_lines + 8, seperator, header_lines + 2]
|
|
||||||
} else {
|
|
||||||
vec![header_lines + 7, header_lines, header_lines + 7, seperator, header_lines + 2]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if tos_needed || pp_needed {
|
|
||||||
text_lines.push(Line::from("By selecting Continue, you confirm that you",));
|
|
||||||
text_lines.push(Line::from("agree to the End User License",));
|
|
||||||
text_lines.push(Line::from("Agreement and applicable Terms of Service.",));
|
|
||||||
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate"));
|
|
||||||
text_lines.push(Line::from("once these changes apply."));
|
|
||||||
} else {
|
|
||||||
text_lines.push(Line::from("By selecting Continue, you confirm that you",));
|
|
||||||
text_lines.push(Line::from("agree to the End User License",));
|
|
||||||
text_lines.push(Line::from("Agreement and applicable Terms of Service.",));
|
|
||||||
}
|
|
||||||
text_lines.push(Line::from("",));
|
|
||||||
text_lines.push(Line::from("Tensamin services require acceptance of the",));
|
|
||||||
text_lines.push(Line::from("Terms of Service and Privacy Policy.",));
|
|
||||||
text_lines.push(Line::from("",));
|
|
||||||
text_lines.push(Line::from("While having a document selected press O to view",));
|
|
||||||
text_lines.push(Line::from("in this UI or press L to open as a link.",));
|
|
||||||
if tos_needed || pp_needed {
|
|
||||||
vec![header_lines + 10, header_lines, header_lines + 11, seperator, header_lines + 2]
|
|
||||||
} else {
|
|
||||||
vec![header_lines + 8, header_lines, header_lines + 9, seperator, header_lines + 2]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
while size.height - 5 < text_lines.len() as u16 {
|
|
||||||
if optional_lines.len() == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
text_lines.remove(optional_lines[0] as usize);
|
|
||||||
optional_lines.remove(0);
|
|
||||||
}
|
|
||||||
needed_height += text_lines.len();
|
|
||||||
|
|
||||||
|
|
||||||
let q_informer = if update_needed {
|
|
||||||
"Q to Exit"
|
|
||||||
} else {
|
|
||||||
"Q to Cancel"
|
|
||||||
};
|
|
||||||
if size.width < 60 || size.height < needed_height as u16 {
|
|
||||||
let width_style = if size.width > 76 {
|
|
||||||
Style::default().fg(Color::Green)
|
|
||||||
} else if size.width >= 60 {
|
|
||||||
Style::default().fg(Color::Yellow)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::Red)
|
|
||||||
};
|
|
||||||
|
|
||||||
let height_style = if size.height > 20 {
|
|
||||||
Style::default().fg(Color::Green)
|
|
||||||
} else if size.height >= (header_lines as u16 + 10) {
|
|
||||||
Style::default().fg(Color::Yellow)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::Red)
|
|
||||||
};
|
|
||||||
|
|
||||||
let warning_text = Text::from(vec![
|
|
||||||
Line::from(vec![
|
|
||||||
Span::raw("Width: "),
|
|
||||||
Span::styled(format!("{}", size.width), width_style),
|
|
||||||
Span::raw(" / 60"),
|
|
||||||
]),
|
|
||||||
Line::from(vec![
|
|
||||||
Span::raw("Height: "),
|
|
||||||
Span::styled(format!("{}", size.height), height_style),
|
|
||||||
Span::raw(format!(" / {}", header_lines + 10)),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
|
||||||
let warning = Paragraph::new(warning_text)
|
|
||||||
.alignment(Alignment::Center)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.title(format!("UI Too Small [{}]", q_informer)),
|
|
||||||
);
|
|
||||||
|
|
||||||
too_small = true;
|
|
||||||
f.render_widget(warning, size);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let consent_block = Paragraph::new(Text::from(text_lines))
|
|
||||||
.block(Block::default().title(format!(" Update Tensamin User Consent [{}] ", q_informer)).borders(Borders::ALL));
|
|
||||||
f.render_widget(consent_block, chunks[0]);
|
|
||||||
|
|
||||||
let downgrade_scenario = tos_needed || pp_needed;
|
|
||||||
draw_buttons(
|
|
||||||
f,
|
|
||||||
chunks[1],
|
|
||||||
focus,
|
|
||||||
(eula, tos && pp),
|
|
||||||
update_needed,
|
|
||||||
downgrade_scenario,
|
|
||||||
pp_needed || tos_needed
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if event::poll(Duration::from_millis(200)).unwrap() {
|
|
||||||
if let Event::Key(key) = event::read().unwrap() {
|
|
||||||
if too_small {
|
|
||||||
if matches!(key.code, KeyCode::Char('q') | KeyCode::Char('Q')) {
|
|
||||||
break UserChoice::Deny;
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut possible_states = vec![Focus::Eula];
|
|
||||||
|
|
||||||
if tos_needed {
|
|
||||||
possible_states.push(Focus::Tos);
|
|
||||||
}
|
|
||||||
if pp_needed {
|
|
||||||
possible_states.push(Focus::Pp);
|
|
||||||
}
|
|
||||||
|
|
||||||
possible_states.push(Focus::Cancel);
|
|
||||||
if eula {
|
|
||||||
possible_states.push(Focus::Continue);
|
|
||||||
if tos && pp && (pp_needed || tos_needed) {
|
|
||||||
possible_states.push(Focus::ContinueAll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Esc => break UserChoice::Deny,
|
|
||||||
KeyCode::Up | KeyCode::Left => focus.prev(&possible_states),
|
|
||||||
KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next(&possible_states),
|
|
||||||
KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny,
|
|
||||||
KeyCode::Char('o') | KeyCode::Char('O') => {
|
|
||||||
let terms_type = match focus {
|
|
||||||
Focus::Eula => Type::EULA,
|
|
||||||
Focus::Tos => Type::TOS,
|
|
||||||
Focus::Pp => Type::PP,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(eula) = get_terms(terms_type.clone()).await {
|
|
||||||
terminal = FileViewer::new(terms_type.to_string(), &eula)
|
|
||||||
.force_popup(terminal);
|
|
||||||
} else {
|
|
||||||
terminal = FileViewer::new(
|
|
||||||
terms_type.to_string(),
|
|
||||||
"### A loading error occured\
|
|
||||||
\nTry reloading this site or retry in a moment.",
|
|
||||||
)
|
|
||||||
.force_popup(terminal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Char('l') | KeyCode::Char('L') => {
|
|
||||||
let terms_type = match focus {
|
|
||||||
Focus::Eula => Type::EULA,
|
|
||||||
Focus::Tos => Type::TOS,
|
|
||||||
Focus::Pp => Type::PP,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = open::that(get_newest_link(terms_type));
|
|
||||||
}
|
|
||||||
KeyCode::Char(' ') | KeyCode::Enter => match focus {
|
|
||||||
Focus::Eula => {
|
|
||||||
eula = !eula;
|
|
||||||
tos = !tos_needed;
|
|
||||||
pp = !pp_needed;
|
|
||||||
}
|
|
||||||
Focus::Tos => {
|
|
||||||
if eula {
|
|
||||||
tos = !tos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Focus::Pp => {
|
|
||||||
if eula {
|
|
||||||
pp = !pp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Focus::Cancel => break UserChoice::Deny,
|
|
||||||
Focus::Continue if eula => break UserChoice::AcceptEULA,
|
|
||||||
Focus::ContinueAll if eula && tos && pp => {
|
|
||||||
break UserChoice::AcceptAll;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ratatui::restore();
|
|
||||||
|
|
||||||
match result {
|
|
||||||
UserChoice::AcceptAll => {
|
|
||||||
consent.accepted_eula = true;
|
|
||||||
consent.accepted_tos = true;
|
|
||||||
consent.accepted_pp = true;
|
|
||||||
if let Some(_) = eula_for_future {
|
|
||||||
consent.future_eula = eula_for_future;
|
|
||||||
}
|
|
||||||
if let Some(_) = tos_for_future {
|
|
||||||
consent.future_tos = tos_for_future;
|
|
||||||
}
|
|
||||||
if let Some(_) = pp_for_future {
|
|
||||||
consent.future_privacy = pp_for_future;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UserChoice::AcceptEULA => {
|
|
||||||
consent.accepted_eula = true;
|
|
||||||
if let Some(_) = eula_for_future {
|
|
||||||
consent.future_eula = eula_for_future;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UserChoice::Deny => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
consent
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue