606 lines
22 KiB
Rust
606 lines
22 KiB
Rust
use crate::{
|
|
terms::{
|
|
md_viewer::FileViewer,
|
|
terms_getter::{Type, get_current_docs, get_link, get_terms},
|
|
},
|
|
util::file_util::{load_file, save_file},
|
|
};
|
|
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;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
pub struct ConsentManager;
|
|
impl ConsentManager {
|
|
pub async fn check() -> (bool, bool) {
|
|
let file = load_file("", "agreements");
|
|
let existing = ConsentUiState::from_str(&file).sanitize();
|
|
|
|
let final_state = if existing.eula {
|
|
existing
|
|
} else {
|
|
let choice = run_consent_ui().await;
|
|
let state = match choice {
|
|
UserChoice::Deny => ConsentUiState::denied(),
|
|
UserChoice::AcceptEULA => ConsentUiState {
|
|
eula: true,
|
|
tos: false,
|
|
pp: false,
|
|
focus: Focus::Cancel,
|
|
},
|
|
UserChoice::AcceptAll => ConsentUiState {
|
|
eula: true,
|
|
tos: true,
|
|
pp: true,
|
|
focus: Focus::Cancel,
|
|
},
|
|
};
|
|
let state = state.sanitize();
|
|
if let Ok(string) = state.to_string().await {
|
|
save_file("", "agreements", &string);
|
|
}
|
|
state
|
|
};
|
|
|
|
(final_state.eula, final_state.pp && final_state.tos)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
enum UserChoice {
|
|
Deny,
|
|
AcceptEULA,
|
|
AcceptAll,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct ConsentUiState {
|
|
eula: bool,
|
|
tos: bool,
|
|
pp: bool,
|
|
focus: Focus,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
enum Focus {
|
|
Eula,
|
|
Tos,
|
|
Pp,
|
|
Cancel,
|
|
Continue,
|
|
ContinueAll,
|
|
}
|
|
|
|
impl ConsentUiState {
|
|
fn denied() -> Self {
|
|
Self {
|
|
eula: false,
|
|
pp: false,
|
|
tos: false,
|
|
focus: Focus::Cancel,
|
|
}
|
|
}
|
|
|
|
fn sanitize(mut self) -> Self {
|
|
if !self.eula {
|
|
self.tos = false;
|
|
self.pp = false;
|
|
}
|
|
self
|
|
}
|
|
|
|
async fn to_string(self) -> Result<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/\
|
|
\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/\
|
|
\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(())
|
|
}
|
|
}
|
|
|
|
fn from_str(s: &str) -> Self {
|
|
let mut eula = false;
|
|
let mut pp = false;
|
|
let mut tos = false;
|
|
|
|
for line in s.lines() {
|
|
if let Some(v) = line.strip_prefix("EULA=") {
|
|
eula = v == "true";
|
|
} else if let Some(v) = line.strip_prefix("ToS=") {
|
|
tos = v == "true";
|
|
} else if let Some(v) = line.strip_prefix("PrivacyPolicy=") {
|
|
pp = v == "true";
|
|
}
|
|
}
|
|
|
|
Self {
|
|
eula,
|
|
pp,
|
|
tos,
|
|
focus: Focus::Cancel,
|
|
}
|
|
.sanitize()
|
|
}
|
|
fn can_continue(&self) -> bool {
|
|
self.eula
|
|
}
|
|
fn can_continue_all(&self) -> bool {
|
|
self.eula && self.pp && self.tos
|
|
}
|
|
|
|
fn next(&mut self) {
|
|
self.focus = match self.focus {
|
|
Focus::Eula => Focus::Tos,
|
|
Focus::Tos => Focus::Pp,
|
|
Focus::Pp => Focus::Cancel,
|
|
Focus::Cancel => {
|
|
if self.can_continue() {
|
|
Focus::Continue
|
|
} else {
|
|
Focus::Eula
|
|
}
|
|
}
|
|
Focus::Continue => Focus::ContinueAll,
|
|
Focus::ContinueAll => Focus::Eula,
|
|
};
|
|
}
|
|
|
|
fn prev(&mut self) {
|
|
self.focus = match self.focus {
|
|
Focus::Eula => Focus::ContinueAll,
|
|
Focus::Tos => Focus::Eula,
|
|
Focus::Pp => Focus::Tos,
|
|
Focus::Cancel => Focus::Pp,
|
|
Focus::Continue => Focus::Cancel,
|
|
Focus::ContinueAll => Focus::Continue,
|
|
};
|
|
}
|
|
}
|
|
|
|
async fn run_consent_ui() -> UserChoice {
|
|
let mut terminal = ratatui::init();
|
|
|
|
let mut state = ConsentUiState {
|
|
eula: false,
|
|
tos: false,
|
|
pp: false,
|
|
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, state.eula, state.focus == Focus::Eula, true),
|
|
checkbox(tos_text, state.tos, state.focus == Focus::Tos, state.eula),
|
|
checkbox(pp_text, state.pp, state.focus == Focus::Pp, state.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], &state);
|
|
})
|
|
.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 => state.prev(),
|
|
KeyCode::Down | KeyCode::Right | KeyCode::Tab => state.next(),
|
|
KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny,
|
|
KeyCode::Char('o') | KeyCode::Char('O') => {
|
|
let terms_type = match state.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",
|
|
)
|
|
.force_popup(terminal);
|
|
}
|
|
}
|
|
KeyCode::Char('l') | KeyCode::Char('L') => {
|
|
let terms_type = match state.focus {
|
|
Focus::Eula => Type::EULA,
|
|
Focus::Tos => Type::TOS,
|
|
Focus::Pp => Type::PP,
|
|
_ => continue,
|
|
};
|
|
|
|
let _ = open::that(get_link(terms_type));
|
|
}
|
|
KeyCode::Char(' ') => match state.focus {
|
|
Focus::Eula => {
|
|
state.eula = !state.eula;
|
|
state.tos = false;
|
|
state.pp = false;
|
|
}
|
|
Focus::Tos => {
|
|
if state.eula {
|
|
state.tos = !state.tos
|
|
}
|
|
}
|
|
Focus::Pp => {
|
|
if state.eula {
|
|
state.pp = !state.pp
|
|
}
|
|
}
|
|
_ => {}
|
|
},
|
|
KeyCode::Enter => match state.focus {
|
|
Focus::Cancel => break UserChoice::Deny,
|
|
Focus::Continue if state.can_continue() => break UserChoice::AcceptEULA,
|
|
Focus::ContinueAll if state.can_continue_all() => {
|
|
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: &ConsentUiState) {
|
|
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 state.focus == Focus::Cancel {
|
|
Style::default()
|
|
.fg(Color::Black)
|
|
.bg(Color::Red)
|
|
.add_modifier(Modifier::BOLD)
|
|
} else {
|
|
Style::default().fg(Color::Red)
|
|
}
|
|
}
|
|
Focus::Continue => {
|
|
if state.focus == Focus::Continue && state.can_continue() {
|
|
Style::default()
|
|
.fg(Color::Black)
|
|
.bg(Color::Green)
|
|
.add_modifier(Modifier::BOLD)
|
|
} else if state.can_continue() {
|
|
Style::default().fg(Color::Green)
|
|
} else {
|
|
Style::default().fg(Color::DarkGray)
|
|
}
|
|
}
|
|
Focus::ContinueAll => {
|
|
if state.focus == Focus::ContinueAll && state.can_continue_all() {
|
|
Style::default()
|
|
.fg(Color::Black)
|
|
.bg(Color::Green)
|
|
.add_modifier(Modifier::BOLD)
|
|
} else if state.can_continue_all() {
|
|
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
|
|
}
|