diff --git a/src/eula/eula_checker.rs b/src/eula/eula_checker.rs deleted file mode 100644 index a1d37fa..0000000 --- a/src/eula/eula_checker.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::util::file_util::{load_file, save_file}; - -pub fn check_eula() -> bool { - let eula = "By changing the value to \"true\" you agree to our end user license agreement and our terms of service!\ - \nYou can find our Terms of service on https://docs.tensamin.net/legal/terms-of-service/.\ - \neula=false"; - let file = load_file("", "eula.txt"); - if file.is_empty() { - save_file("", "eula.txt", eula); - return false; - } - - if file.contains("eula=false") { - false - } else if file.contains("eula=true") { - true - } else { - false - } -} -pub fn accept_eula() { - let eula = "By changing the value to \"true\" you agree to our end user license agreement and our terms of service!\ - \nYou can find our Terms of service on https://docs.tensamin.net/legal/terms-of-service/.\ - \neula=true"; - save_file("", "eula.txt", eula); -} diff --git a/src/eula/mod.rs b/src/eula/mod.rs index a3ee485..950fe79 100644 --- a/src/eula/mod.rs +++ b/src/eula/mod.rs @@ -1 +1 @@ -pub mod eula_checker; \ No newline at end of file +pub mod terms_checker; diff --git a/src/eula/terms_checker.rs b/src/eula/terms_checker.rs new file mode 100644 index 0000000..b17660c --- /dev/null +++ b/src/eula/terms_checker.rs @@ -0,0 +1,519 @@ +use crate::util::file_util::{load_file, save_file}; +use crossterm::{ + event::{self, Event, KeyCode}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, +}; +use ratatui::{ + Terminal, + backend::CrosstermBackend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Paragraph}, +}; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::{io, time::Duration}; + +pub struct ConsentManager; +impl ConsentManager { + pub 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(); + 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(); + save_file("", "agreements", &state.to_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 + } + + fn to_string(self) -> String { + let current_secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + format!( + "EULA=true indicates that you read and accepted the End User Licence agreement. You can find our EULA at https://docs.tensamin.net/legal/eula/ + \nEULA={}\ + \nPrivacyPolicy=true indicates that you read and accepted the Privacy Policy. You can find our Privacy Policy at https://docs.tensamin.net/legal/privacy-policy/ + \nPrivacyPolicy={}\ + \nToS=true indicates that you read and accepted the Terms of Service. You can find our Terms of Service at https://docs.tensamin.net/legal/terms-of-service/ + \nToS={}\ + \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={}", + self.eula, self.pp, self.tos, current_secs + ) + } + + 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, + }; + } +} + +fn run_consent_ui() -> UserChoice { + enable_raw_mode().unwrap(); + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen).unwrap(); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend).unwrap(); + + let mut state = ConsentUiState { + eula: false, + tos: false, + pp: false, + focus: Focus::Eula, + }; + + let result = loop { + terminal + .draw(|f| { + let size = f.area(); + + let mut needed_height = 5; + + let margin = if size.width > 80 && size.height > 18 { + 2 + } else if size.width > 78 && size.height > 16 { + 1 + } else { + 0 + }; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(7), Constraint::Length(3)]) + .margin(margin) + .split(size); + let eula_text = if size.width < 76 { + "EULA ¹ (https://docs.tensamin.net/legal/eula/)" + } else { + "End User Licence Agreement ¹ (https://docs.tensamin.net/legal/eula/)" + }; + let tos_text = if size.width < 76 { + "ToS ² (https://docs.tensamin.net/legal/terms-of-service/)" + } else { + "Terms of Service ² (https://docs.tensamin.net/legal/terms-of-service/)" + }; + let pp_text = if size.width < 76 { + "PP ² (https://docs.tensamin.net/legal/privacy-policy/)" + } else { + "Privacy Policy ² (https://docs.tensamin.net/legal/privacy-policy/)" + }; + + let (mut optional_lines, agree_lines): (Vec, Vec<&str>) = + if size.width > 131 { + ( + vec![7, 3, 4], + vec![ + "", + "By selecting Continue, you confirm that you have read 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.", + ] + ) + } else if size.width > 67 { + ( + vec![8, 3, 4], + vec![ + "", + "By selecting Continue, you confirm that you have read and agree", + "to the End User License Agreement and applicable Terms of Service", + "", + "Tensamin services require acceptance of the ToS and Privacy Policy", + ] + ) + } else { + ( + vec![9, 3, 4], + vec![ + "", + "By selecting Continue, you confirm that you have", + "read 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" + ] + ) + }; + 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 < 63 || size.height < needed_height as u16 { + let width_style = if size.width > 76 { + Style::default().fg(Color::Green) + } else if size.width >= 63 { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Red) + }; + + let height_style = if size.height < needed_height as u16 { + Style::default().fg(Color::Red) + } else if size.height >= 11 { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Green) + }; + + let warning_text = Text::from(vec![ + Line::from(vec![ + Span::raw("Width: "), + Span::styled(format!("{}", size.width), width_style), + Span::raw(" / 63"), + ]), + Line::from(vec![ + Span::raw("Height: "), + Span::styled(format!("{}", size.height), height_style), + Span::raw(format!(" / {}", needed_height)), + ]), + ]); + + let warning = Paragraph::new(warning_text) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .title("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("Consent").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() { + match key.code { + KeyCode::Esc => break UserChoice::Deny, + KeyCode::Up => state.prev(), + KeyCode::Down | KeyCode::Tab => state.next(), + KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny, + 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; + } + _ => {} + }, + _ => {} + } + } + } + }; + + disable_raw_mode().unwrap(); + execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap(); + terminal.show_cursor().unwrap(); + + 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 = 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() && state.pp { + 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 { + let mut widths = vec![0; min_widths.len()]; + let mut remaining: Vec = (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 +} diff --git a/src/main.rs b/src/main.rs index 429a361..2fb1077 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use json::{self}; use once_cell::sync::Lazy; use pnet::datalink::NetworkInterface; use std::sync::Arc; @@ -20,6 +19,7 @@ mod util; use crate::communities::community_manager; use crate::communities::interactables::registry; +use crate::eula::terms_checker; use crate::gui::app_state::AppState; use crate::gui::input_handler; use crate::gui::log_panel; @@ -48,10 +48,19 @@ async fn main() { *SHUTDOWN.write().await = false; // EULA - //if !eula_checker::check_eula() { - // println!("Please accept the end user license agreement before launching!"); - // return; - //} + let (tos, pp) = terms_checker::ConsentManager::check(); + if !tos { + println!("You need to accept our End User Licence Agreement before launching!"); + println!("You can find this at 'agreements'!"); + return; + } + if !pp { + println!( + "Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!" + ); + println!("You can find this at 'agreements'!"); + return; + } // LANGUAGE PACK if let Err(e) = language_creator::create_languages() {