[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::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;
|
||||||
use crate::terms::terms_checker;
|
|
||||||
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::has_dir;
|
use crate::util::file_util::has_dir;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
terms::terms_checker,
|
terms::{doc::Doc, terms_checker, terms_getter::Type},
|
||||||
terms::terms_getter::get_current_docs,
|
|
||||||
util::file_util::{load_file, save_file},
|
util::file_util::{load_file, save_file},
|
||||||
};
|
};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
@ -11,31 +10,19 @@ impl ConsentManager {
|
||||||
let file = load_file("", "agreements");
|
let file = load_file("", "agreements");
|
||||||
let existing = ConsentState::from_str(&file).sanitize();
|
let existing = ConsentState::from_str(&file).sanitize();
|
||||||
|
|
||||||
let final_state = if existing.eula {
|
let final_state = if existing.accepted_eula {
|
||||||
existing
|
existing
|
||||||
} else {
|
} else {
|
||||||
let choice = terms_checker::run_consent_ui().await;
|
let state = terms_checker::run_consent_ui(existing).await.sanitize();
|
||||||
let state = match choice {
|
let string = state.clone().to_string().await;
|
||||||
UserChoice::Deny => ConsentState::denied(),
|
save_file("", "agreements", &string);
|
||||||
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 {
|
|
||||||
save_file("", "agreements", &string);
|
|
||||||
}
|
|
||||||
state
|
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,
|
AcceptAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ConsentState {
|
pub struct ConsentState {
|
||||||
pub eula: bool,
|
pub eula: Option<Doc>,
|
||||||
pub tos: bool,
|
pub accepted_eula: bool,
|
||||||
pub pp: bool,
|
pub tos: Option<Doc>,
|
||||||
|
pub accepted_tos: bool,
|
||||||
|
pub pp: Option<Doc>,
|
||||||
|
pub accepted_pp: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConsentState {
|
impl ConsentState {
|
||||||
fn denied() -> Self {
|
|
||||||
Self {
|
|
||||||
eula: false,
|
|
||||||
pp: false,
|
|
||||||
tos: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sanitize(mut self) -> Self {
|
fn sanitize(mut self) -> Self {
|
||||||
if !self.eula {
|
if !self.accepted_eula {
|
||||||
self.tos = false;
|
self.accepted_tos = false;
|
||||||
self.pp = false;
|
self.accepted_pp = false;
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn to_string(self) -> Result<String, ()> {
|
async fn to_string(self) -> String {
|
||||||
let current_secs = SystemTime::now()
|
let current_secs = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs();
|
.as_secs();
|
||||||
|
|
||||||
if let Some((eula, tos, pp)) = get_current_docs().await {
|
let mut file_out: String = format!(
|
||||||
Ok(format!(
|
"This file reflects the current consent state used by the application.\
|
||||||
"\
|
\nIt may be regenerated or overwritten by the application.\
|
||||||
\"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/\
|
\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={}\
|
||||||
\nEULA-VERSION={}\
|
\nEULA-VERSION={}\
|
||||||
\nEULA-HASH={}\
|
\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/\
|
", self.accepted_eula, eula.get_version(), eula.get_hash()));
|
||||||
\nPrivacyPolicy={}\
|
|
||||||
\nPrivacyPolicy-VERSION={}\
|
if self.accepted_tos
|
||||||
\nPrivacyPolicy-HASH={}\
|
&& let Some(tos) = self.tos
|
||||||
\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/\
|
{
|
||||||
\nToS={}\
|
file_out.push_str(&format!("\
|
||||||
\nToS-VERSION={}\
|
\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-HASH={}\
|
\nToS={}\
|
||||||
\nThis file reflects the current consent state used by the application.\
|
\nToS-VERSION={}\
|
||||||
\nIt may be regenerated or overwritten by the application.\
|
\nToS-HASH={}\
|
||||||
\nThis file was last edited by Tensamin at:\
|
", self.accepted_tos, tos.get_version(), tos.get_hash()));
|
||||||
\nUNIX-SECOND={}\
|
}
|
||||||
",
|
if self.accepted_pp
|
||||||
self.eula,
|
&& let Some(pp) = self.pp
|
||||||
eula.get_version(),
|
{
|
||||||
eula.get_hash(),
|
file_out.push_str(&format!("\
|
||||||
self.tos,
|
\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/\
|
||||||
tos.get_version(),
|
\nPrivacy-Policy={}\
|
||||||
tos.get_hash(),
|
\nPrivacy-Policy-VERSION={}\
|
||||||
self.pp,
|
\nPrivacy-Policy-HASH={}\
|
||||||
pp.get_version(),
|
", self.accepted_pp, pp.get_version(), pp.get_hash()));
|
||||||
pp.get_hash(),
|
}
|
||||||
current_secs
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
Err(())
|
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 {
|
fn from_str(s: &str) -> Self {
|
||||||
let mut eula = false;
|
let mut eula = false;
|
||||||
|
let mut eula_version = String::new();
|
||||||
|
let mut eula_hash = String::new();
|
||||||
let mut pp = false;
|
let mut pp = false;
|
||||||
|
let mut pp_version = String::new();
|
||||||
|
let mut pp_hash = String::new();
|
||||||
let mut tos = false;
|
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() {
|
for line in s.lines() {
|
||||||
if let Some(v) = line.strip_prefix("EULA=") {
|
if let Some(v) = line.strip_prefix("EULA=") {
|
||||||
eula = v == "true";
|
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=") {
|
} else if let Some(v) = line.strip_prefix("ToS=") {
|
||||||
tos = v == "true";
|
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=") {
|
} else if let Some(v) = line.strip_prefix("PrivacyPolicy=") {
|
||||||
pp = v == "true";
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let unix: u64 = unix.parse::<u64>().unwrap_or(0);
|
||||||
Self { eula, pp, tos }.sanitize()
|
Self {
|
||||||
}
|
accepted_eula: eula,
|
||||||
pub fn can_continue(&self) -> bool {
|
eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)),
|
||||||
self.eula
|
accepted_pp: pp,
|
||||||
}
|
pp: Some(Doc::new(pp_version, pp_hash, Type::PP, unix)),
|
||||||
pub fn can_continue_all(&self) -> bool {
|
accepted_tos: tos,
|
||||||
self.eula && self.pp && self.tos
|
tos: Some(Doc::new(tos_version, tos_hash, Type::TOS, unix)),
|
||||||
|
}
|
||||||
|
.sanitize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use json::{JsonValue, object::Object};
|
||||||
|
|
||||||
use crate::{terms::terms_getter::Type, util::file_util::load_file};
|
use crate::{terms::terms_getter::Type, util::file_util::load_file};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct Doc {
|
pub struct Doc {
|
||||||
version: String,
|
version: String,
|
||||||
|
|
@ -13,6 +13,15 @@ pub struct Doc {
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl Doc {
|
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 {
|
pub fn get_version(&self) -> String {
|
||||||
self.version.clone()
|
self.version.clone()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
use crate::terms::consent_state::ConsentState;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum Focus {
|
pub enum Focus {
|
||||||
Eula,
|
Eula,
|
||||||
|
|
@ -10,20 +8,20 @@ pub enum Focus {
|
||||||
ContinueAll,
|
ContinueAll,
|
||||||
}
|
}
|
||||||
impl Focus {
|
impl Focus {
|
||||||
pub fn next(&mut self, state: ConsentState) {
|
pub fn next(&mut self, state: (bool, bool, bool)) {
|
||||||
*self = match self {
|
*self = match self {
|
||||||
Focus::Eula => Focus::Tos,
|
Focus::Eula => Focus::Tos,
|
||||||
Focus::Tos => Focus::Pp,
|
Focus::Tos => Focus::Pp,
|
||||||
Focus::Pp => Focus::Cancel,
|
Focus::Pp => Focus::Cancel,
|
||||||
Focus::Cancel => {
|
Focus::Cancel => {
|
||||||
if state.can_continue() {
|
if state.0 {
|
||||||
Focus::Continue
|
Focus::Continue
|
||||||
} else {
|
} else {
|
||||||
Focus::Eula
|
Focus::Eula
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Focus::Continue => {
|
Focus::Continue => {
|
||||||
if state.can_continue_all() {
|
if state.0 && state.1 && state.2 {
|
||||||
Focus::ContinueAll
|
Focus::ContinueAll
|
||||||
} else {
|
} else {
|
||||||
Focus::Eula
|
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 {
|
*self = match self {
|
||||||
Focus::Eula => {
|
Focus::Eula => {
|
||||||
if state.can_continue_all() {
|
if state.0 && state.1 && state.2 {
|
||||||
Focus::ContinueAll
|
Focus::ContinueAll
|
||||||
} else if state.can_continue() {
|
} else if state.0 {
|
||||||
Focus::Continue
|
Focus::Continue
|
||||||
} else {
|
} else {
|
||||||
Focus::Cancel
|
Focus::Cancel
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ pub mod focus;
|
||||||
pub mod md_viewer;
|
pub mod md_viewer;
|
||||||
pub mod terms_checker;
|
pub mod terms_checker;
|
||||||
pub mod terms_getter;
|
pub mod terms_getter;
|
||||||
|
pub mod terms_updater;
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,10 @@ use ratatui::{
|
||||||
};
|
};
|
||||||
use std::time::Duration;
|
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 terminal = ratatui::init();
|
||||||
|
|
||||||
let mut state = ConsentState {
|
let (mut eula, mut tos, mut pp) = (false, false, false);
|
||||||
eula: false,
|
|
||||||
tos: false,
|
|
||||||
pp: false,
|
|
||||||
};
|
|
||||||
let mut focus = Focus::Eula;
|
let mut focus = Focus::Eula;
|
||||||
|
|
||||||
let result = loop {
|
let result = loop {
|
||||||
|
|
@ -129,9 +125,9 @@ pub async fn run_consent_ui() -> UserChoice {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let mut text_lines = vec![
|
let mut text_lines = vec![
|
||||||
checkbox(eula_text, state.eula, focus == Focus::Eula, true),
|
checkbox(eula_text, eula, focus == Focus::Eula, true),
|
||||||
checkbox(tos_text, state.tos, focus == Focus::Tos, state.eula),
|
checkbox(tos_text, tos, focus == Focus::Tos, eula),
|
||||||
checkbox(pp_text, state.pp, focus == Focus::Pp, state.eula),
|
checkbox(pp_text, pp, focus == Focus::Pp, eula),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("¹ Necessary, ² Optional"),
|
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));
|
.block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL));
|
||||||
f.render_widget(consent_block, chunks[0]);
|
f.render_widget(consent_block, chunks[0]);
|
||||||
|
|
||||||
draw_buttons(f, chunks[1], &state);
|
draw_buttons(f, chunks[1], focus, (eula, tos, pp));
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
@ -213,8 +209,8 @@ pub async fn run_consent_ui() -> UserChoice {
|
||||||
}
|
}
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => break UserChoice::Deny,
|
KeyCode::Esc => break UserChoice::Deny,
|
||||||
KeyCode::Up | KeyCode::Left => focus.prev(state),
|
KeyCode::Up | KeyCode::Left => focus.prev((eula, tos, pp)),
|
||||||
KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next(state),
|
KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next((eula, tos, pp)),
|
||||||
KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny,
|
KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny,
|
||||||
KeyCode::Char('o') | KeyCode::Char('O') => {
|
KeyCode::Char('o') | KeyCode::Char('O') => {
|
||||||
let terms_type = match focus {
|
let terms_type = match focus {
|
||||||
|
|
@ -248,26 +244,26 @@ pub async fn run_consent_ui() -> UserChoice {
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => match focus {
|
KeyCode::Char(' ') => match focus {
|
||||||
Focus::Eula => {
|
Focus::Eula => {
|
||||||
state.eula = !state.eula;
|
eula = !eula;
|
||||||
state.tos = false;
|
tos = false;
|
||||||
state.pp = false;
|
pp = false;
|
||||||
}
|
}
|
||||||
Focus::Tos => {
|
Focus::Tos => {
|
||||||
if state.eula {
|
if eula {
|
||||||
state.tos = !state.tos
|
tos = !tos
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Focus::Pp => {
|
Focus::Pp => {
|
||||||
if state.eula {
|
if eula {
|
||||||
state.pp = !state.pp
|
pp = !pp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
KeyCode::Enter => match focus {
|
KeyCode::Enter => match focus {
|
||||||
Focus::Cancel => break UserChoice::Deny,
|
Focus::Cancel => break UserChoice::Deny,
|
||||||
Focus::Continue if state.can_continue() => break UserChoice::AcceptEULA,
|
Focus::Continue if eula => break UserChoice::AcceptEULA,
|
||||||
Focus::ContinueAll if state.can_continue_all() => {
|
Focus::ContinueAll if eula && tos && pp => {
|
||||||
break UserChoice::AcceptAll;
|
break UserChoice::AcceptAll;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -279,7 +275,24 @@ pub async fn run_consent_ui() -> UserChoice {
|
||||||
};
|
};
|
||||||
ratatui::restore();
|
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)]
|
#[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));
|
.block(Block::default().borders(Borders::ALL));
|
||||||
f.render_widget(p, area);
|
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![
|
let buttons = vec![
|
||||||
("[Q] Cancel", Focus::Cancel),
|
("[Q] Cancel", Focus::Cancel),
|
||||||
("Continue", Focus::Continue),
|
("Continue", Focus::Continue),
|
||||||
|
|
@ -343,9 +361,11 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentState) {
|
||||||
};
|
};
|
||||||
x += width;
|
x += width;
|
||||||
|
|
||||||
|
let is_focused = current_focus == *focus;
|
||||||
|
|
||||||
let style = match focus {
|
let style = match focus {
|
||||||
Focus::Cancel => {
|
Focus::Cancel => {
|
||||||
if focus == &Focus::Cancel {
|
if is_focused {
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Black)
|
.fg(Color::Black)
|
||||||
.bg(Color::Red)
|
.bg(Color::Red)
|
||||||
|
|
@ -354,30 +374,33 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentState) {
|
||||||
Style::default().fg(Color::Red)
|
Style::default().fg(Color::Red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Focus::Continue => {
|
Focus::Continue => {
|
||||||
if focus == &Focus::Continue && state.can_continue() {
|
if is_focused && state.0 {
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Black)
|
.fg(Color::Black)
|
||||||
.bg(Color::Green)
|
.bg(Color::Green)
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else if state.can_continue() {
|
} else if state.0 {
|
||||||
Style::default().fg(Color::Green)
|
Style::default().fg(Color::Green) // enabled but not focused
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::DarkGray)
|
Style::default().fg(Color::DarkGray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Focus::ContinueAll => {
|
Focus::ContinueAll => {
|
||||||
if focus == &Focus::ContinueAll && state.can_continue_all() {
|
if is_focused && state.0 && state.1 && state.2 {
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Black)
|
.fg(Color::Black)
|
||||||
.bg(Color::Green)
|
.bg(Color::Green)
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else if state.can_continue_all() {
|
} else if state.0 && state.1 && state.2 {
|
||||||
Style::default().fg(Color::Green)
|
Style::default().fg(Color::Green)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::DarkGray)
|
Style::default().fg(Color::DarkGray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => Style::default().fg(Color::DarkGray),
|
_ => Style::default().fg(Color::DarkGray),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use json::JsonValue::Object;
|
||||||
|
|
||||||
use crate::terms::doc::Doc;
|
use crate::terms::doc::Doc;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Type {
|
pub enum Type {
|
||||||
EULA,
|
EULA,
|
||||||
TOS,
|
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