[WIP] Terms update support
This commit is contained in:
parent
a81f80b191
commit
3574aca541
8 changed files with 584 additions and 115 deletions
|
|
@ -29,7 +29,6 @@ use crate::langu::language_manager::format;
|
|||
use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection};
|
||||
use crate::server::server::start;
|
||||
use crate::terms::consent_state;
|
||||
use crate::terms::terms_checker;
|
||||
use crate::users::user_manager;
|
||||
use crate::util::config_util::CONFIG;
|
||||
use crate::util::file_util::has_dir;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use crate::{
|
||||
terms::terms_checker,
|
||||
terms::terms_getter::get_current_docs,
|
||||
terms::{doc::Doc, terms_checker, terms_getter::Type},
|
||||
util::file_util::{load_file, save_file},
|
||||
};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
|
@ -11,31 +10,19 @@ impl ConsentManager {
|
|||
let file = load_file("", "agreements");
|
||||
let existing = ConsentState::from_str(&file).sanitize();
|
||||
|
||||
let final_state = if existing.eula {
|
||||
let final_state = if existing.accepted_eula {
|
||||
existing
|
||||
} else {
|
||||
let choice = terms_checker::run_consent_ui().await;
|
||||
let state = match choice {
|
||||
UserChoice::Deny => ConsentState::denied(),
|
||||
UserChoice::AcceptEULA => ConsentState {
|
||||
eula: true,
|
||||
tos: false,
|
||||
pp: false,
|
||||
},
|
||||
UserChoice::AcceptAll => ConsentState {
|
||||
eula: true,
|
||||
tos: true,
|
||||
pp: true,
|
||||
},
|
||||
};
|
||||
let state = state.sanitize();
|
||||
if let Ok(string) = state.to_string().await {
|
||||
let state = terms_checker::run_consent_ui(existing).await.sanitize();
|
||||
let string = state.clone().to_string().await;
|
||||
save_file("", "agreements", &string);
|
||||
}
|
||||
state
|
||||
};
|
||||
|
||||
(final_state.eula, final_state.pp && final_state.tos)
|
||||
(
|
||||
final_state.accepted_eula,
|
||||
final_state.accepted_pp && final_state.accepted_tos,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,93 +33,123 @@ pub enum UserChoice {
|
|||
AcceptAll,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConsentState {
|
||||
pub eula: bool,
|
||||
pub tos: bool,
|
||||
pub pp: bool,
|
||||
pub eula: Option<Doc>,
|
||||
pub accepted_eula: bool,
|
||||
pub tos: Option<Doc>,
|
||||
pub accepted_tos: bool,
|
||||
pub pp: Option<Doc>,
|
||||
pub accepted_pp: bool,
|
||||
}
|
||||
|
||||
impl ConsentState {
|
||||
fn denied() -> Self {
|
||||
Self {
|
||||
eula: false,
|
||||
pp: false,
|
||||
tos: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize(mut self) -> Self {
|
||||
if !self.eula {
|
||||
self.tos = false;
|
||||
self.pp = false;
|
||||
if !self.accepted_eula {
|
||||
self.accepted_tos = false;
|
||||
self.accepted_pp = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
async fn to_string(self) -> Result<String, ()> {
|
||||
async fn to_string(self) -> String {
|
||||
let current_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
if let Some((eula, tos, pp)) = get_current_docs().await {
|
||||
Ok(format!(
|
||||
"\
|
||||
\"EULA=true\" indicates that you read, understood and accepted the End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\
|
||||
let mut file_out: String = format!(
|
||||
"This file reflects the current consent state used by the application.\
|
||||
\nIt may be regenerated or overwritten by the application.\
|
||||
\nThis file was last edited by Tensamin at:\
|
||||
\nUNIX-SECOND={}",
|
||||
current_secs
|
||||
);
|
||||
|
||||
if self.accepted_eula
|
||||
&& let Some(eula) = self.eula
|
||||
{
|
||||
file_out.push_str(&format!("\
|
||||
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\
|
||||
\nEULA={}\
|
||||
\nEULA-VERSION={}\
|
||||
\nEULA-HASH={}\
|
||||
\n\"PrivacyPolicy=true\" indicates that you read, understood and accepted the Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy-policy/\
|
||||
\nPrivacyPolicy={}\
|
||||
\nPrivacyPolicy-VERSION={}\
|
||||
\nPrivacyPolicy-HASH={}\
|
||||
\n\"ToS=true\" indicates that you read, understood and accepted the Terms of Service. You can find our Terms of Service at https://legal.tensamin.net/terms-of-service/\
|
||||
", self.accepted_eula, eula.get_version(), eula.get_hash()));
|
||||
|
||||
if self.accepted_tos
|
||||
&& let Some(tos) = self.tos
|
||||
{
|
||||
file_out.push_str(&format!("\
|
||||
\n\"ToS=true\" indicates that you read, understood and accepted Tensamin's Terms of Service. You can find our Terms of Serivce at https://legal.tensamin.net/tos/\
|
||||
\nToS={}\
|
||||
\nToS-VERSION={}\
|
||||
\nToS-HASH={}\
|
||||
\nThis file reflects the current consent state used by the application.\
|
||||
\nIt may be regenerated or overwritten by the application.\
|
||||
\nThis file was last edited by Tensamin at:\
|
||||
\nUNIX-SECOND={}\
|
||||
",
|
||||
self.eula,
|
||||
eula.get_version(),
|
||||
eula.get_hash(),
|
||||
self.tos,
|
||||
tos.get_version(),
|
||||
tos.get_hash(),
|
||||
self.pp,
|
||||
pp.get_version(),
|
||||
pp.get_hash(),
|
||||
current_secs
|
||||
))
|
||||
} else {
|
||||
Err(())
|
||||
", self.accepted_tos, tos.get_version(), tos.get_hash()));
|
||||
}
|
||||
if self.accepted_pp
|
||||
&& let Some(pp) = self.pp
|
||||
{
|
||||
file_out.push_str(&format!("\
|
||||
\n\"Privacy-Policy=true\" indicates that you read, understood and accepted Tensamin's Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy/\
|
||||
\nPrivacy-Policy={}\
|
||||
\nPrivacy-Policy-VERSION={}\
|
||||
\nPrivacy-Policy-HASH={}\
|
||||
", self.accepted_pp, pp.get_version(), pp.get_hash()));
|
||||
}
|
||||
} else {
|
||||
file_out.push_str("\
|
||||
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.tensamin.net/eula/\
|
||||
\nEULA=false\
|
||||
");
|
||||
}
|
||||
file_out
|
||||
}
|
||||
|
||||
fn from_str(s: &str) -> Self {
|
||||
let mut eula = false;
|
||||
let mut eula_version = String::new();
|
||||
let mut eula_hash = String::new();
|
||||
let mut pp = false;
|
||||
let mut pp_version = String::new();
|
||||
let mut pp_hash = String::new();
|
||||
let mut tos = false;
|
||||
let mut tos_version = String::new();
|
||||
let mut tos_hash = String::new();
|
||||
|
||||
let mut unix = String::new();
|
||||
|
||||
for line in s.lines() {
|
||||
if let Some(v) = line.strip_prefix("EULA=") {
|
||||
eula = v == "true";
|
||||
} else if let Some(v) = line.strip_prefix("EULA-VERSION=") {
|
||||
eula_version = v.to_string();
|
||||
} else if let Some(v) = line.strip_prefix("EULA-HASH=") {
|
||||
eula_hash = v.to_string();
|
||||
} else if let Some(v) = line.strip_prefix("ToS=") {
|
||||
tos = v == "true";
|
||||
} else if let Some(v) = line.strip_prefix("ToS-VERSION=") {
|
||||
tos_version = v.to_string();
|
||||
} else if let Some(v) = line.strip_prefix("ToS-HASH=") {
|
||||
tos_hash = v.to_string();
|
||||
} else if let Some(v) = line.strip_prefix("PrivacyPolicy=") {
|
||||
pp = v == "true";
|
||||
} else if let Some(v) = line.strip_prefix("PrivacyPolicy-VERSION=") {
|
||||
pp_version = v.to_string();
|
||||
} else if let Some(v) = line.strip_prefix("PrivacyPolicy-HASH=") {
|
||||
pp_hash = v.to_string();
|
||||
} else if let Some(v) = line.strip_prefix("UNIX=") {
|
||||
unix = v.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
Self { eula, pp, tos }.sanitize()
|
||||
let unix: u64 = unix.parse::<u64>().unwrap_or(0);
|
||||
Self {
|
||||
accepted_eula: eula,
|
||||
eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)),
|
||||
accepted_pp: pp,
|
||||
pp: Some(Doc::new(pp_version, pp_hash, Type::PP, unix)),
|
||||
accepted_tos: tos,
|
||||
tos: Some(Doc::new(tos_version, tos_hash, Type::TOS, unix)),
|
||||
}
|
||||
pub fn can_continue(&self) -> bool {
|
||||
self.eula
|
||||
}
|
||||
pub fn can_continue_all(&self) -> bool {
|
||||
self.eula && self.pp && self.tos
|
||||
.sanitize()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use json::{JsonValue, object::Object};
|
|||
|
||||
use crate::{terms::terms_getter::Type, util::file_util::load_file};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(unused)]
|
||||
pub struct Doc {
|
||||
version: String,
|
||||
|
|
@ -13,6 +13,15 @@ pub struct Doc {
|
|||
|
||||
#[allow(dead_code)]
|
||||
impl Doc {
|
||||
pub fn new(version: String, hash: String, doc_type: Type, timestamp: u64) -> Doc {
|
||||
Doc {
|
||||
version,
|
||||
hash,
|
||||
doc_type,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_version(&self) -> String {
|
||||
self.version.clone()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use crate::terms::consent_state::ConsentState;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Focus {
|
||||
Eula,
|
||||
|
|
@ -10,20 +8,20 @@ pub enum Focus {
|
|||
ContinueAll,
|
||||
}
|
||||
impl Focus {
|
||||
pub fn next(&mut self, state: ConsentState) {
|
||||
pub fn next(&mut self, state: (bool, bool, bool)) {
|
||||
*self = match self {
|
||||
Focus::Eula => Focus::Tos,
|
||||
Focus::Tos => Focus::Pp,
|
||||
Focus::Pp => Focus::Cancel,
|
||||
Focus::Cancel => {
|
||||
if state.can_continue() {
|
||||
if state.0 {
|
||||
Focus::Continue
|
||||
} else {
|
||||
Focus::Eula
|
||||
}
|
||||
}
|
||||
Focus::Continue => {
|
||||
if state.can_continue_all() {
|
||||
if state.0 && state.1 && state.2 {
|
||||
Focus::ContinueAll
|
||||
} else {
|
||||
Focus::Eula
|
||||
|
|
@ -33,12 +31,12 @@ impl Focus {
|
|||
};
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, state: ConsentState) {
|
||||
pub fn prev(&mut self, state: (bool, bool, bool)) {
|
||||
*self = match self {
|
||||
Focus::Eula => {
|
||||
if state.can_continue_all() {
|
||||
if state.0 && state.1 && state.2 {
|
||||
Focus::ContinueAll
|
||||
} else if state.can_continue() {
|
||||
} else if state.0 {
|
||||
Focus::Continue
|
||||
} else {
|
||||
Focus::Cancel
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ pub mod focus;
|
|||
pub mod md_viewer;
|
||||
pub mod terms_checker;
|
||||
pub mod terms_getter;
|
||||
pub mod terms_updater;
|
||||
|
|
|
|||
|
|
@ -13,14 +13,10 @@ use ratatui::{
|
|||
};
|
||||
use std::time::Duration;
|
||||
|
||||
pub async fn run_consent_ui() -> UserChoice {
|
||||
pub async fn run_consent_ui(consent: ConsentState) -> ConsentState {
|
||||
let mut terminal = ratatui::init();
|
||||
|
||||
let mut state = ConsentState {
|
||||
eula: false,
|
||||
tos: false,
|
||||
pp: false,
|
||||
};
|
||||
let (mut eula, mut tos, mut pp) = (false, false, false);
|
||||
let mut focus = Focus::Eula;
|
||||
|
||||
let result = loop {
|
||||
|
|
@ -129,9 +125,9 @@ pub async fn run_consent_ui() -> UserChoice {
|
|||
)
|
||||
};
|
||||
let mut text_lines = vec![
|
||||
checkbox(eula_text, state.eula, focus == Focus::Eula, true),
|
||||
checkbox(tos_text, state.tos, focus == Focus::Tos, state.eula),
|
||||
checkbox(pp_text, state.pp, focus == Focus::Pp, state.eula),
|
||||
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, ² Optional"),
|
||||
];
|
||||
|
|
@ -198,7 +194,7 @@ pub async fn run_consent_ui() -> UserChoice {
|
|||
.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], &state);
|
||||
draw_buttons(f, chunks[1], focus, (eula, tos, pp));
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -213,8 +209,8 @@ pub async fn run_consent_ui() -> UserChoice {
|
|||
}
|
||||
match key.code {
|
||||
KeyCode::Esc => break UserChoice::Deny,
|
||||
KeyCode::Up | KeyCode::Left => focus.prev(state),
|
||||
KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next(state),
|
||||
KeyCode::Up | KeyCode::Left => focus.prev((eula, tos, pp)),
|
||||
KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next((eula, tos, pp)),
|
||||
KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny,
|
||||
KeyCode::Char('o') | KeyCode::Char('O') => {
|
||||
let terms_type = match focus {
|
||||
|
|
@ -248,26 +244,26 @@ pub async fn run_consent_ui() -> UserChoice {
|
|||
}
|
||||
KeyCode::Char(' ') => match focus {
|
||||
Focus::Eula => {
|
||||
state.eula = !state.eula;
|
||||
state.tos = false;
|
||||
state.pp = false;
|
||||
eula = !eula;
|
||||
tos = false;
|
||||
pp = false;
|
||||
}
|
||||
Focus::Tos => {
|
||||
if state.eula {
|
||||
state.tos = !state.tos
|
||||
if eula {
|
||||
tos = !tos
|
||||
}
|
||||
}
|
||||
Focus::Pp => {
|
||||
if state.eula {
|
||||
state.pp = !state.pp
|
||||
if eula {
|
||||
pp = !pp
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Enter => match focus {
|
||||
Focus::Cancel => break UserChoice::Deny,
|
||||
Focus::Continue if state.can_continue() => break UserChoice::AcceptEULA,
|
||||
Focus::ContinueAll if state.can_continue_all() => {
|
||||
Focus::Continue if eula => break UserChoice::AcceptEULA,
|
||||
Focus::ContinueAll if eula && tos && pp => {
|
||||
break UserChoice::AcceptAll;
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -279,7 +275,24 @@ pub async fn run_consent_ui() -> UserChoice {
|
|||
};
|
||||
ratatui::restore();
|
||||
|
||||
result
|
||||
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 == true;
|
||||
consent.accepted_pp == true;
|
||||
}
|
||||
UserChoice::Deny => {
|
||||
consent.accepted_eula == false;
|
||||
consent.accepted_tos == false;
|
||||
consent.accepted_pp == false;
|
||||
}
|
||||
};
|
||||
consent
|
||||
}
|
||||
|
||||
#[allow(mismatched_lifetime_syntaxes)]
|
||||
|
|
@ -317,7 +330,12 @@ fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) {
|
|||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(p, area);
|
||||
}
|
||||
fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentState) {
|
||||
fn draw_buttons(
|
||||
f: &mut ratatui::Frame,
|
||||
area: Rect,
|
||||
current_focus: Focus,
|
||||
state: (bool, bool, bool),
|
||||
) {
|
||||
let buttons = vec![
|
||||
("[Q] Cancel", Focus::Cancel),
|
||||
("Continue", Focus::Continue),
|
||||
|
|
@ -343,9 +361,11 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentState) {
|
|||
};
|
||||
x += width;
|
||||
|
||||
let is_focused = current_focus == *focus;
|
||||
|
||||
let style = match focus {
|
||||
Focus::Cancel => {
|
||||
if focus == &Focus::Cancel {
|
||||
if is_focused {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Red)
|
||||
|
|
@ -354,30 +374,33 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentState) {
|
|||
Style::default().fg(Color::Red)
|
||||
}
|
||||
}
|
||||
|
||||
Focus::Continue => {
|
||||
if focus == &Focus::Continue && state.can_continue() {
|
||||
if is_focused && state.0 {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if state.can_continue() {
|
||||
Style::default().fg(Color::Green)
|
||||
} else if state.0 {
|
||||
Style::default().fg(Color::Green) // enabled but not focused
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
}
|
||||
}
|
||||
|
||||
Focus::ContinueAll => {
|
||||
if focus == &Focus::ContinueAll && state.can_continue_all() {
|
||||
if is_focused && state.0 && state.1 && state.2 {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if state.can_continue_all() {
|
||||
} else if state.0 && state.1 && state.2 {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
}
|
||||
}
|
||||
|
||||
_ => Style::default().fg(Color::DarkGray),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use json::JsonValue::Object;
|
|||
|
||||
use crate::terms::doc::Doc;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Type {
|
||||
EULA,
|
||||
TOS,
|
||||
|
|
|
|||
422
src/terms/terms_updater.rs
Normal file
422
src/terms/terms_updater.rs
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
use crate::terms::{
|
||||
consent_state::UserChoice,
|
||||
focus::Focus,
|
||||
md_viewer::FileViewer,
|
||||
terms_getter::{Type, get_link, get_terms},
|
||||
};
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
pub async fn run_consent_ui() -> UserChoice {
|
||||
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 {}/12", size.width, size.height)), size);
|
||||
return;
|
||||
}
|
||||
|
||||
let max_width = 150;
|
||||
let max_height = 20;
|
||||
|
||||
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![7, 3, 7, 4],
|
||||
vec![
|
||||
"",
|
||||
"By selecting Continue, you confirm that you have read, understood and 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![8, 3, 8, 4],
|
||||
vec![
|
||||
"",
|
||||
"By selecting Continue, you confirm that you have read, understood and 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![8, 3, 8, 4],
|
||||
vec![
|
||||
"",
|
||||
"By selecting Continue, you confirm that you have read, understood and",
|
||||
"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![9, 3, 10, 4],
|
||||
vec![
|
||||
"",
|
||||
"By selecting Continue, you confirm that you have",
|
||||
"read, understood and 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, ² Optional"),
|
||||
];
|
||||
|
||||
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 < 12 {
|
||||
Style::default().fg(Color::Red)
|
||||
} else if size.height > 19 {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::Yellow)
|
||||
};
|
||||
|
||||
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!(" / 12")),
|
||||
]),
|
||||
]);
|
||||
|
||||
let warning = Paragraph::new(warning_text)
|
||||
.alignment(Alignment::Center)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("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], (eula, tos, pp));
|
||||
})
|
||||
.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;
|
||||
}
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Esc => break UserChoice::Deny,
|
||||
KeyCode::Up | KeyCode::Left => focus.prev((eula, tos, pp)),
|
||||
KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next((eula, tos, pp)),
|
||||
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();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[allow(mismatched_lifetime_syntaxes)]
|
||||
fn checkbox(label: &str, checked: bool, active: bool, allowed: bool) -> Line {
|
||||
let box_char = if checked { "[x]" } else { "[ ]" };
|
||||
let (box_style, text_style) = if active {
|
||||
if allowed {
|
||||
(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Style::default().fg(Color::Gray),
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
(Style::default(), Style::default())
|
||||
};
|
||||
Line::from(vec![
|
||||
Span::styled(box_char, box_style),
|
||||
Span::raw(" "),
|
||||
Span::styled(label, text_style),
|
||||
])
|
||||
}
|
||||
|
||||
fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) {
|
||||
let p = Paragraph::new(Span::styled(label, style))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(p, area);
|
||||
}
|
||||
fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: (bool, bool, bool)) {
|
||||
let buttons = vec![
|
||||
("[Q] Cancel", Focus::Cancel),
|
||||
("Continue", Focus::Continue),
|
||||
("Continue with Tensamin Services", Focus::ContinueAll),
|
||||
];
|
||||
|
||||
let padding = 2;
|
||||
let min_widths: Vec<u16> = buttons
|
||||
.iter()
|
||||
.map(|(label, _)| label.len() as u16 + padding)
|
||||
.collect();
|
||||
|
||||
let widths = compute_widths(area.width, &min_widths);
|
||||
|
||||
let mut x = area.x;
|
||||
|
||||
for ((label, focus), width) in buttons.iter().zip(widths) {
|
||||
let chunk = Rect {
|
||||
x,
|
||||
y: area.y,
|
||||
width,
|
||||
height: area.height,
|
||||
};
|
||||
x += width;
|
||||
|
||||
let style = match focus {
|
||||
Focus::Cancel => {
|
||||
if focus == &Focus::Cancel {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
}
|
||||
}
|
||||
Focus::Continue => {
|
||||
if focus == &Focus::Continue && state.0 {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if state.0 && state.1 && state.2 {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
}
|
||||
}
|
||||
Focus::ContinueAll => {
|
||||
if focus == &Focus::ContinueAll && state.0 && state.1 && state.2 {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if state.0 && state.1 && state.2 {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
}
|
||||
}
|
||||
_ => Style::default().fg(Color::DarkGray),
|
||||
};
|
||||
|
||||
draw_button(f, chunk, label, style);
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec<u16> {
|
||||
let mut widths = vec![0; min_widths.len()];
|
||||
let mut remaining: Vec<usize> = (0..min_widths.len()).collect();
|
||||
|
||||
let mut remaining_width = area_width;
|
||||
|
||||
while !remaining.is_empty() {
|
||||
let count = remaining.len() as u16;
|
||||
let equal = remaining_width / count;
|
||||
|
||||
let mut clamped = Vec::new();
|
||||
|
||||
for &i in &remaining {
|
||||
if min_widths[i] > equal {
|
||||
widths[i] = min_widths[i];
|
||||
remaining_width -= min_widths[i];
|
||||
clamped.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if clamped.is_empty() {
|
||||
let mut remainder = remaining_width % count;
|
||||
for &i in &remaining {
|
||||
widths[i] = equal
|
||||
+ if remainder > 0 {
|
||||
remainder -= 1;
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
remaining.retain(|i| !clamped.contains(i));
|
||||
}
|
||||
|
||||
widths
|
||||
}
|
||||
Loading…
Reference in a new issue