[Add] Terms Updater
This commit is contained in:
parent
b8029ca829
commit
bfefa46839
9 changed files with 507 additions and 468 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -351,9 +351,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
version = "0.4.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
|
|
@ -1552,6 +1552,7 @@ dependencies = [
|
|||
"axum",
|
||||
"base64",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"cmake",
|
||||
"color-eyre",
|
||||
"crossterm 0.29.0",
|
||||
|
|
|
|||
|
|
@ -59,3 +59,4 @@ strum_macros = "0.27.2"
|
|||
ratatui = "0.30.0"
|
||||
ratatui_input = "0.1.3"
|
||||
open = "5.3.3"
|
||||
chrono = "0.4.43"
|
||||
|
|
|
|||
175
src/terms/buttons.rs
Normal file
175
src/terms/buttons.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
use crate::terms::focus::Focus;
|
||||
|
||||
#[allow(mismatched_lifetime_syntaxes)]
|
||||
pub 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),
|
||||
])
|
||||
}
|
||||
|
||||
pub 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);
|
||||
}
|
||||
pub fn draw_buttons(
|
||||
f: &mut ratatui::Frame,
|
||||
area: Rect,
|
||||
current_focus: Focus,
|
||||
state: (bool, bool),
|
||||
update_needed: bool,
|
||||
downgrade_scenario: bool,
|
||||
tos_or_privacy: bool,
|
||||
) {
|
||||
let cancel_text = if update_needed {
|
||||
"[Q] Quit"
|
||||
} else {
|
||||
"[Q] Not now"
|
||||
};
|
||||
let continue_text = if downgrade_scenario {
|
||||
"Downgrade"
|
||||
} else {
|
||||
"Continue"
|
||||
};
|
||||
let mut buttons = vec![
|
||||
(cancel_text, Focus::Cancel),
|
||||
(continue_text, Focus::Continue),
|
||||
];
|
||||
if tos_or_privacy {
|
||||
buttons.push(("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 is_focused = current_focus == *focus;
|
||||
|
||||
let style = match focus {
|
||||
Focus::Cancel => {
|
||||
if is_focused {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
}
|
||||
}
|
||||
|
||||
Focus::Continue => {
|
||||
if is_focused && state.0 {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if state.0 {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
}
|
||||
}
|
||||
|
||||
Focus::ContinueAll => {
|
||||
if is_focused && state.1 {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if state.1 {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
}
|
||||
}
|
||||
|
||||
_ => Style::default().fg(Color::DarkGray),
|
||||
};
|
||||
|
||||
draw_button(f, chunk, label, style);
|
||||
}
|
||||
}
|
||||
pub 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
|
||||
}
|
||||
|
|
@ -13,14 +13,15 @@ pub struct ConsentManager;
|
|||
impl ConsentManager {
|
||||
pub async fn check() -> (bool, bool) {
|
||||
let file = load_file("", "agreements");
|
||||
let mut file_state = ConsentState::from_str(&file).sanitize();
|
||||
let (mut file_state, changed) = ConsentState::from_str(&file).sanitize();
|
||||
|
||||
if changed {
|
||||
save_file("", "agreements", &file_state.to_string());
|
||||
}
|
||||
|
||||
if !file_state.accepted_eula {
|
||||
file_state = terms_checker::run_consent_ui(file_state).await;
|
||||
|
||||
println!("{}", &file_state.clone().sanitize().to_string());
|
||||
println!("-----------------------");
|
||||
|
||||
save_file("", "agreements", &file_state.to_string());
|
||||
}
|
||||
|
||||
|
|
@ -34,17 +35,16 @@ impl ConsentManager {
|
|||
|| matches!(privacy_update, UpdateDecision::Forced(_));
|
||||
|
||||
let updater_logic = async move {
|
||||
let state_to_update = terms_updater::run_consent_ui(
|
||||
let (state_to_update, _) = terms_updater::run_consent_ui(
|
||||
file_state,
|
||||
eula_update,
|
||||
tos_update,
|
||||
privacy_update,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.sanitize();
|
||||
|
||||
println!("{}", &state_to_update.clone().sanitize().to_string());
|
||||
|
||||
save_file("", "agreements", &state_to_update.sanitize().to_string());
|
||||
save_file("", "agreements", &state_to_update.to_string());
|
||||
};
|
||||
// ======================================================== //
|
||||
// TODO: Implement UI Drawing order, draw update As PoPup //
|
||||
|
|
@ -56,7 +56,11 @@ impl ConsentManager {
|
|||
//}
|
||||
}
|
||||
|
||||
let final_state = ConsentState::from_str(&load_file("", "agreements")).sanitize();
|
||||
let file = load_file("", "agreements");
|
||||
let (final_state, changed) = ConsentState::from_str(&file).sanitize();
|
||||
if changed {
|
||||
save_file("", "agreements", &final_state.to_string());
|
||||
}
|
||||
(
|
||||
final_state.accepted_eula,
|
||||
final_state.accepted_tos && final_state.accepted_pp,
|
||||
|
|
@ -72,8 +76,10 @@ impl ConsentManager {
|
|||
UpdateDecision,
|
||||
)> {
|
||||
let file = load_file("", "agreements");
|
||||
let accepted_state = ConsentState::from_str(&file).sanitize();
|
||||
|
||||
let (accepted_state, changed) = ConsentState::from_str(&file).sanitize();
|
||||
if changed {
|
||||
save_file("", "agreements", &accepted_state.to_string());
|
||||
}
|
||||
if let (
|
||||
Some((current_eula, current_tos, current_privacy)),
|
||||
Some((newest_eula, newest_tos, newest_privacy)),
|
||||
|
|
@ -82,46 +88,68 @@ impl ConsentManager {
|
|||
let eula_update: UpdateDecision = if current_eula.equals_some(&accepted_state.eula) {
|
||||
if current_eula.equals(&newest_eula) {
|
||||
UpdateDecision::NoChange
|
||||
} else {
|
||||
if newest_eula.equals_some(&accepted_state.future_eula) {
|
||||
UpdateDecision::NoChange
|
||||
} else {
|
||||
UpdateDecision::Future {
|
||||
newest: newest_eula,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if newest_eula.equals_some(&accepted_state.eula) {
|
||||
UpdateDecision::NoChange
|
||||
} else {
|
||||
UpdateDecision::Forced(current_eula)
|
||||
};
|
||||
|
||||
let tos_update: UpdateDecision = if newest_tos.equals_some(&accepted_state.tos) {
|
||||
let tos_update: UpdateDecision = if !accepted_state.accepted_tos
|
||||
|| newest_tos.equals_some(&accepted_state.tos)
|
||||
{
|
||||
UpdateDecision::NoChange
|
||||
} else if accepted_state.accepted_tos && current_tos.equals_some(&accepted_state.tos) {
|
||||
if current_tos.equals(&newest_tos) {
|
||||
UpdateDecision::NoChange
|
||||
} else {
|
||||
if newest_tos.equals_some(&accepted_state.future_tos) {
|
||||
UpdateDecision::NoChange
|
||||
} else {
|
||||
UpdateDecision::Future { newest: newest_tos }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UpdateDecision::Forced(current_tos)
|
||||
};
|
||||
|
||||
let privacy_update: UpdateDecision =
|
||||
if newest_privacy.equals_some(&accepted_state.privacy) {
|
||||
let privacy_update: UpdateDecision = if !accepted_state.accepted_pp
|
||||
|| newest_privacy.equals_some(&accepted_state.privacy)
|
||||
{
|
||||
UpdateDecision::NoChange
|
||||
} else if accepted_state.accepted_pp
|
||||
&& current_privacy.equals_some(&accepted_state.privacy)
|
||||
{
|
||||
if current_privacy.equals(&newest_privacy) {
|
||||
UpdateDecision::NoChange
|
||||
} else {
|
||||
if newest_privacy.equals_some(&accepted_state.future_privacy) {
|
||||
UpdateDecision::NoChange
|
||||
} else {
|
||||
UpdateDecision::Future {
|
||||
newest: newest_privacy,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UpdateDecision::Forced(current_privacy)
|
||||
};
|
||||
Some((eula_update, tos_update, privacy_update))
|
||||
match (&eula_update, &tos_update, &privacy_update) {
|
||||
(
|
||||
&UpdateDecision::NoChange,
|
||||
&UpdateDecision::NoChange,
|
||||
&UpdateDecision::NoChange,
|
||||
) => None,
|
||||
_ => Some((eula_update, tos_update, privacy_update)),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -158,12 +186,41 @@ pub struct ConsentState {
|
|||
}
|
||||
|
||||
impl ConsentState {
|
||||
fn sanitize(mut self) -> Self {
|
||||
fn sanitize(mut self) -> (Self, bool) {
|
||||
let mut changed = false;
|
||||
let current_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
if let Some(future_eula) = self.future_eula.clone() {
|
||||
if future_eula.get_time() < current_secs {
|
||||
self.eula = Some(future_eula);
|
||||
self.future_eula = None;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if let Some(future_tos) = self.future_tos.clone() {
|
||||
if future_tos.get_time() < current_secs {
|
||||
self.tos = Some(future_tos);
|
||||
self.future_tos = None;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if let Some(future_privacy) = self.future_privacy.clone() {
|
||||
if future_privacy.get_time() < current_secs {
|
||||
self.privacy = Some(future_privacy);
|
||||
self.future_privacy = None;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.accepted_eula {
|
||||
self.accepted_tos = false;
|
||||
self.accepted_pp = false;
|
||||
changed = true;
|
||||
}
|
||||
self
|
||||
(self, changed)
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
|
|
@ -325,7 +382,7 @@ impl ConsentState {
|
|||
let future_eula_time = future_eula_time.parse::<u64>().unwrap_or(0);
|
||||
let future_tos_time = future_tos_time.parse::<u64>().unwrap_or(0);
|
||||
let future_pp_time = future_pp_time.parse::<u64>().unwrap_or(0);
|
||||
Self {
|
||||
let (state, _) = Self {
|
||||
accepted_eula: eula,
|
||||
eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)),
|
||||
accepted_pp: pp,
|
||||
|
|
@ -363,6 +420,8 @@ impl ConsentState {
|
|||
None
|
||||
},
|
||||
}
|
||||
.sanitize()
|
||||
.sanitize();
|
||||
|
||||
state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ impl FileViewer {
|
|||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(self.title.as_str()),
|
||||
.title(format!("{} - [Q to close]", self.title.as_str(),)),
|
||||
)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((self.scroll, 0));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod buttons;
|
||||
pub mod consent_state;
|
||||
pub mod doc;
|
||||
pub mod focus;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::terms::{
|
||||
buttons::{checkbox, draw_buttons},
|
||||
consent_state::{ConsentState, UserChoice},
|
||||
focus::Focus,
|
||||
md_viewer::FileViewer,
|
||||
|
|
@ -7,7 +8,7 @@ use crate::terms::{
|
|||
use crossterm::event::{self, Event, KeyCode};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
style::{Color, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
|
@ -29,12 +30,12 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
|
||||
if size.height < 6
|
||||
|| size.width < 27 {
|
||||
f.render_widget(Line::from(format!("too small {}/63 by {}/12", size.width, size.height)), size);
|
||||
f.render_widget(Line::from(format!("too small {}/63 by {}/13", size.width, size.height)), size);
|
||||
return;
|
||||
}
|
||||
|
||||
let max_width = 150;
|
||||
let max_height = 20;
|
||||
let max_height = 26;
|
||||
|
||||
let content_width = if max_width < size.width { max_width } else { size.width} ;
|
||||
let content_height = if max_height < size.height { max_height } else { size.height} ;
|
||||
|
|
@ -52,28 +53,28 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
height: content_height,
|
||||
});
|
||||
let eula_text = if size.width < 70 {
|
||||
"EULA ¹ (https://legal.tensamin.net/eula/newest/)"
|
||||
"EULA ¹ (https://legal.tensamin.net/eula/)"
|
||||
} else {
|
||||
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)"
|
||||
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)"
|
||||
};
|
||||
let tos_text = if size.width < 72 {
|
||||
"ToS ² (https://legal.tensamin.net/terms-of-service/newest/)"
|
||||
"ToS ² (https://legal.tensamin.net/terms-of-service/)"
|
||||
} else {
|
||||
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)"
|
||||
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)"
|
||||
};
|
||||
let pp_text = if size.width < 68 {
|
||||
"PP ² (https://legal.tensamin.net/privacy-policy/newest/)"
|
||||
"PP ² (https://legal.tensamin.net/privacy-policy/)"
|
||||
} else {
|
||||
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)"
|
||||
"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![8, 3, 8, 5],
|
||||
vec![
|
||||
"",
|
||||
"By selecting Continue, you confirm that you have read, understood and agree to the End User License Agreement and applicable Terms of Service.",
|
||||
"By selecting Continue, you confirm that you have agree to the End User License Agreement and applicable Terms of Service.",
|
||||
"",
|
||||
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
|
||||
"",
|
||||
|
|
@ -82,10 +83,10 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
)
|
||||
} else if size.width > 92 {
|
||||
(
|
||||
vec![8, 3, 8, 4],
|
||||
vec![9, 3, 9, 5],
|
||||
vec![
|
||||
"",
|
||||
"By selecting Continue, you confirm that you have read, understood and agree to the End User",
|
||||
"By selecting Continue, you confirm that you have agree to the End User",
|
||||
"License Agreement and applicable Terms of Service.",
|
||||
"",
|
||||
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
|
||||
|
|
@ -95,7 +96,7 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
)
|
||||
} else if size.width > 73 {
|
||||
(
|
||||
vec![8, 3, 8, 4],
|
||||
vec![9, 3, 9, 5],
|
||||
vec![
|
||||
"",
|
||||
"By selecting Continue, you confirm that you have read, understood and",
|
||||
|
|
@ -109,11 +110,11 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
)
|
||||
} else {
|
||||
(
|
||||
vec![9, 3, 10, 4],
|
||||
vec![10, 3, 11, 5],
|
||||
vec![
|
||||
"",
|
||||
"By selecting Continue, you confirm that you have",
|
||||
"read, understood and agree to the End User License",
|
||||
"agree to the End User License",
|
||||
"Agreement and applicable Terms of Service.",
|
||||
"",
|
||||
"Tensamin services require acceptance of the",
|
||||
|
|
@ -129,9 +130,9 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
checkbox(tos_text, tos, focus == Focus::Tos, eula),
|
||||
checkbox(pp_text, pp, focus == Focus::Pp, eula),
|
||||
Line::from(""),
|
||||
Line::from("¹ Necessary, ² Optional"),
|
||||
Line::from("¹ Necessary– required to run the program"),
|
||||
Line::from("² Optional – required only for Tensamin services")
|
||||
];
|
||||
|
||||
for line in agree_lines {
|
||||
text_lines.insert(text_lines.len(), Line::from(line));
|
||||
}
|
||||
|
|
@ -156,12 +157,12 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
Style::default().fg(Color::Red)
|
||||
};
|
||||
|
||||
let height_style = if size.height < 12 {
|
||||
Style::default().fg(Color::Red)
|
||||
} else if size.height > 19 {
|
||||
let height_style = if size.height > 19 {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
} else if size.height >= 13 {
|
||||
Style::default().fg(Color::Yellow)
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
};
|
||||
|
||||
let warning_text = Text::from(vec![
|
||||
|
|
@ -173,7 +174,7 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
Line::from(vec![
|
||||
Span::raw("Height: "),
|
||||
Span::styled(format!("{}", size.height), height_style),
|
||||
Span::raw(format!(" / 12")),
|
||||
Span::raw(format!(" / 13")),
|
||||
]),
|
||||
]);
|
||||
|
||||
|
|
@ -182,7 +183,7 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("UI Too Small (Q to Quit)"),
|
||||
.title(format!("UI Too Small [Q to Quit]")),
|
||||
);
|
||||
|
||||
too_small = true;
|
||||
|
|
@ -194,7 +195,7 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
.block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL));
|
||||
f.render_widget(consent_block, chunks[0]);
|
||||
|
||||
draw_buttons(f, chunks[1], focus, (eula, tos, pp));
|
||||
draw_buttons(f, chunks[1], focus, (eula, tos && pp), true, false, true);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -296,8 +297,8 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
}
|
||||
UserChoice::AcceptEULA => {
|
||||
consent.accepted_eula = true;
|
||||
consent.accepted_tos = true;
|
||||
consent.accepted_pp = true;
|
||||
consent.accepted_tos = false;
|
||||
consent.accepted_pp = false;
|
||||
}
|
||||
UserChoice::Deny => {
|
||||
consent.accepted_eula = false;
|
||||
|
|
@ -308,155 +309,3 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
|
|||
|
||||
consent
|
||||
}
|
||||
|
||||
#[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,
|
||||
current_focus: Focus,
|
||||
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 is_focused = current_focus == *focus;
|
||||
|
||||
let style = match focus {
|
||||
Focus::Cancel => {
|
||||
if is_focused {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
}
|
||||
}
|
||||
|
||||
Focus::Continue => {
|
||||
if is_focused && state.0 {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if state.0 {
|
||||
Style::default().fg(Color::Green) // enabled but not focused
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
}
|
||||
}
|
||||
|
||||
Focus::ContinueAll => {
|
||||
if is_focused && 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ impl Type {
|
|||
pub fn get_link(terms_type: Type) -> String {
|
||||
format!("https://legal.tensamin.net/{}/", terms_type.to_str())
|
||||
}
|
||||
pub fn get_newest_link(terms_type: Type) -> String {
|
||||
format!("https://legal.tensamin.net/{}/newest/", terms_type.to_str())
|
||||
}
|
||||
|
||||
pub async fn get_current_docs() -> Option<(Doc, Doc, Doc)> {
|
||||
let body = reqwest::get("https://legal.tensamin.net/api/current/")
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
use crate::terms::{
|
||||
buttons::{checkbox, draw_buttons},
|
||||
consent_state::{ConsentState, UpdateDecision, UserChoice},
|
||||
doc::Doc,
|
||||
focus::Focus,
|
||||
md_viewer::FileViewer,
|
||||
terms_getter::{Type, get_link, get_terms},
|
||||
terms_getter::{Type, get_newest_link, get_terms},
|
||||
};
|
||||
use chrono::{Local, TimeZone, Utc};
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
style::{Color, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
pub async fn run_consent_ui(
|
||||
mut consent: ConsentState,
|
||||
consent_eula: UpdateDecision,
|
||||
|
|
@ -38,9 +39,18 @@ pub async fn run_consent_ui(
|
|||
UpdateDecision::Future { newest } => (true, true, Some(newest)),
|
||||
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
|
||||
};
|
||||
let update_needed = eula_needed || tos_needed || pp_needed;
|
||||
let update_needed =
|
||||
(eula_needed && !eula_future) || (tos_needed && !tos_future) || (pp_needed && !pp_future);
|
||||
let (mut eula, mut tos, mut pp) = (!eula_needed, !tos_needed, !pp_needed);
|
||||
let mut focus = Focus::Eula;
|
||||
let mut focus = if eula_needed {
|
||||
Focus::Eula
|
||||
} else if tos_needed {
|
||||
Focus::Tos
|
||||
} else if pp_needed {
|
||||
Focus::Pp
|
||||
} else {
|
||||
Focus::Cancel
|
||||
};
|
||||
|
||||
let result = loop {
|
||||
let mut too_small = false;
|
||||
|
|
@ -52,12 +62,12 @@ pub async fn run_consent_ui(
|
|||
|
||||
if size.height < 6
|
||||
|| size.width < 27 {
|
||||
f.render_widget(Line::from(format!("too small {}/63 by {}/12", size.width, size.height)), size);
|
||||
f.render_widget(Line::from(format!("too small {}/63 by {}/15", size.width, size.height)), size);
|
||||
return;
|
||||
}
|
||||
|
||||
let max_width = 150;
|
||||
let max_height = 20;
|
||||
let max_height = 30;
|
||||
|
||||
let content_width = if max_width < size.width { max_width } else { size.width} ;
|
||||
let content_height = if max_height < size.height { max_height } else { size.height} ;
|
||||
|
|
@ -76,18 +86,46 @@ pub async fn run_consent_ui(
|
|||
});
|
||||
|
||||
|
||||
let mut text_lines = Vec::new();
|
||||
|
||||
let mut text_lines: Vec<Line> = Vec::new();
|
||||
let mut header_lines = 0;
|
||||
let seperator = if size.width > 72 {
|
||||
header_lines += 3;
|
||||
text_lines.push(Line::from("You previously accepted earlier versions of Tensamin’s legal documents."));
|
||||
text_lines.push(Line::from("Some of them have been updated and are listed below for your review."));
|
||||
text_lines.push(Line::from(""));
|
||||
2
|
||||
} else {
|
||||
header_lines += 5;
|
||||
text_lines.push(Line::from("You previously accepted earlier "));
|
||||
text_lines.push(Line::from("versions of Tensamin’s legal documents."));
|
||||
text_lines.push(Line::from("Some of them have been updated and"));
|
||||
text_lines.push(Line::from("are listed below for your review."));
|
||||
text_lines.push(Line::from(""));
|
||||
4
|
||||
};
|
||||
|
||||
if eula_needed {
|
||||
header_lines += 1;
|
||||
if eula_future {
|
||||
if size.width < 70 {
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox("EULA ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
|
||||
|
||||
header_lines += 1;
|
||||
let unix_timestamp = eula_for_future.clone().unwrap().get_time() as i64;
|
||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||
} else {
|
||||
text_lines.push(checkbox("End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
|
||||
|
||||
header_lines += 1;
|
||||
let unix_timestamp = eula_for_future.clone().unwrap().get_time() as i64;
|
||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||
}
|
||||
} else {
|
||||
if size.width < 70 {
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox("EULA ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
|
||||
} else {
|
||||
text_lines.push(checkbox("End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
|
||||
|
|
@ -96,15 +134,28 @@ pub async fn run_consent_ui(
|
|||
}
|
||||
|
||||
if tos_needed {
|
||||
header_lines += 1;
|
||||
if tos_future {
|
||||
if size.width < 70 {
|
||||
text_lines.push(checkbox("ToS ²³ (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox("ToS ²³ (https://legal.tensamin.net/tos/newest/)", tos, focus == Focus::Tos, eula));
|
||||
|
||||
header_lines += 1;
|
||||
let unix_timestamp = tos_for_future.clone().unwrap().get_time() as i64;
|
||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||
} else {
|
||||
text_lines.push(checkbox("Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
|
||||
|
||||
header_lines += 1;
|
||||
let unix_timestamp = tos_for_future.clone().unwrap().get_time() as i64;
|
||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||
}
|
||||
} else {
|
||||
if size.width < 70 {
|
||||
text_lines.push(checkbox("ToS ² (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox("ToS ² (https://legal.tensamin.net/tos/newest/)", tos, focus == Focus::Tos, eula));
|
||||
} else {
|
||||
text_lines.push(checkbox("Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
|
||||
}
|
||||
|
|
@ -112,15 +163,28 @@ pub async fn run_consent_ui(
|
|||
}
|
||||
|
||||
if pp_needed {
|
||||
header_lines += 1;
|
||||
if pp_future {
|
||||
if size.width < 70 {
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox("PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
|
||||
|
||||
header_lines += 1;
|
||||
let unix_timestamp = pp_for_future.clone().unwrap().get_time() as i64;
|
||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||
} else {
|
||||
text_lines.push(checkbox("Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
|
||||
|
||||
header_lines += 1;
|
||||
let unix_timestamp = pp_for_future.clone().unwrap().get_time() as i64;
|
||||
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
|
||||
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
|
||||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||
}
|
||||
} else {
|
||||
if size.width < 70 {
|
||||
text_lines.push(checkbox("PP ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, pp));
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox("PP ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
|
||||
} else {
|
||||
text_lines.push(checkbox("Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
|
||||
}
|
||||
|
|
@ -128,70 +192,100 @@ pub async fn run_consent_ui(
|
|||
}
|
||||
|
||||
text_lines.push(Line::from(""));
|
||||
text_lines.push(Line::from("¹ Necessary, ² Optional, ³ Future"));
|
||||
text_lines.push(Line::from("¹ Necessary to run the program"));
|
||||
text_lines.push(Line::from("² Optional - only for Tensamin services"));
|
||||
if size.width < 100 {
|
||||
text_lines.push(Line::from("³ Future version - consent stored now, takes effect later"));
|
||||
} else {
|
||||
text_lines.push(Line::from("³ Future version - You’ll continue using this version, automatically updated when changes apply."));
|
||||
}
|
||||
|
||||
text_lines.push(Line::from(""));
|
||||
|
||||
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) =
|
||||
let mut optional_lines: Vec<i16> =
|
||||
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.",
|
||||
]
|
||||
)
|
||||
if tos_needed || pp_needed {
|
||||
text_lines.push(Line::from("By selecting Downgrade, you confirm that you have agree to the End User License Agreement and applicable Terms of Service."));
|
||||
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate once these changes apply."));
|
||||
} else {
|
||||
(
|
||||
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.",
|
||||
]
|
||||
)
|
||||
text_lines.push(Line::from("By selecting Continue, you confirm that you have agree to the End User License Agreement and applicable Terms of Service."));
|
||||
}
|
||||
text_lines.push(Line::from(""));
|
||||
text_lines.push(Line::from("Tensamin services require acceptance of the Terms of Service and Privacy Policy."));
|
||||
text_lines.push(Line::from(""));
|
||||
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link."));
|
||||
|
||||
if tos_needed || pp_needed {
|
||||
vec![header_lines + 7, header_lines, header_lines + 7, seperator, header_lines + 2]
|
||||
} else {
|
||||
vec![header_lines + 6, header_lines, header_lines + 6, seperator, header_lines + 2]
|
||||
}
|
||||
} else if size.width > 92 {
|
||||
if tos_needed || pp_needed {
|
||||
text_lines.push(Line::from("By selecting Continue, you confirm that you have agree to the End User"));
|
||||
text_lines.push(Line::from("License Agreement and applicable Terms of Service."));
|
||||
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate once these changes apply."));
|
||||
} else {
|
||||
text_lines.push(Line::from("By selecting Continue, you confirm that you have agree to the End User"));
|
||||
text_lines.push(Line::from("License Agreement and applicable Terms of Service."));
|
||||
}
|
||||
|
||||
text_lines.push(Line::from(""));
|
||||
text_lines.push(Line::from("Tensamin services require acceptance of the Terms of Service and Privacy Policy."));
|
||||
text_lines.push(Line::from(""));
|
||||
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link."));
|
||||
|
||||
if tos_needed || pp_needed {
|
||||
vec![header_lines + 8, header_lines, header_lines + 8, seperator, header_lines + 2]
|
||||
} else {
|
||||
vec![header_lines + 7, header_lines, header_lines + 7, seperator, header_lines + 2]
|
||||
}
|
||||
} else if size.width > 73 {
|
||||
if tos_needed || pp_needed {
|
||||
text_lines.push(Line::from("By selecting Continue, you confirm that you have read, understood and"));
|
||||
text_lines.push(Line::from("agree to the End User License Agreement and applicable Terms of Service."));
|
||||
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate once these changes apply."));
|
||||
} else {
|
||||
text_lines.push(Line::from("By selecting Continue, you confirm that you have read, understood and"));
|
||||
text_lines.push(Line::from("agree to the End User License Agreement and applicable Terms of Service."));
|
||||
}
|
||||
text_lines.push(Line::from("",));
|
||||
text_lines.push(Line::from("Tensamin services require acceptance of the ToS and Privacy Policy.",));
|
||||
text_lines.push(Line::from("",));
|
||||
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L",));
|
||||
text_lines.push(Line::from("to open as a link.",));
|
||||
|
||||
if tos_needed || pp_needed {
|
||||
vec![header_lines + 8, header_lines, header_lines + 8, seperator, header_lines + 2]
|
||||
} else {
|
||||
vec![header_lines + 7, header_lines, header_lines + 7, seperator, header_lines + 2]
|
||||
}
|
||||
} else {
|
||||
if tos_needed || pp_needed {
|
||||
text_lines.push(Line::from("By selecting Continue, you confirm that you have",));
|
||||
text_lines.push(Line::from("agree to the End User License",));
|
||||
text_lines.push(Line::from("Agreement and applicable Terms of Service.",));
|
||||
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate"));
|
||||
text_lines.push(Line::from("once these changes apply."));
|
||||
} else {
|
||||
text_lines.push(Line::from("By selecting Continue, you confirm that you have",));
|
||||
text_lines.push(Line::from("agree to the End User License",));
|
||||
text_lines.push(Line::from("Agreement and applicable Terms of Service.",));
|
||||
}
|
||||
text_lines.push(Line::from("",));
|
||||
text_lines.push(Line::from("Tensamin services require acceptance of the",));
|
||||
text_lines.push(Line::from("Terms of Service and Privacy Policy.",));
|
||||
text_lines.push(Line::from("",));
|
||||
text_lines.push(Line::from("While having a document selected press O to view",));
|
||||
text_lines.push(Line::from("in this UI or press L to open as a link.",));
|
||||
if tos_needed || pp_needed {
|
||||
vec![header_lines + 10, header_lines, header_lines + 11, seperator, header_lines + 2]
|
||||
} else {
|
||||
vec![header_lines + 8, header_lines, header_lines + 9, seperator, header_lines + 2]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
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;
|
||||
|
|
@ -202,7 +296,11 @@ pub async fn run_consent_ui(
|
|||
needed_height += text_lines.len();
|
||||
|
||||
|
||||
|
||||
let q_informer = if update_needed {
|
||||
"Q to Exit"
|
||||
} else {
|
||||
"Q to Cancel"
|
||||
};
|
||||
if size.width < 60 || size.height < needed_height as u16 {
|
||||
let width_style = if size.width > 76 {
|
||||
Style::default().fg(Color::Green)
|
||||
|
|
@ -212,12 +310,12 @@ pub async fn run_consent_ui(
|
|||
Style::default().fg(Color::Red)
|
||||
};
|
||||
|
||||
let height_style = if size.height < 12 {
|
||||
Style::default().fg(Color::Red)
|
||||
} else if size.height > 19 {
|
||||
let height_style = if size.height > 20 {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
} else if size.height >= (header_lines as u16 + 10) {
|
||||
Style::default().fg(Color::Yellow)
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
};
|
||||
|
||||
let warning_text = Text::from(vec![
|
||||
|
|
@ -229,16 +327,17 @@ pub async fn run_consent_ui(
|
|||
Line::from(vec![
|
||||
Span::raw("Height: "),
|
||||
Span::styled(format!("{}", size.height), height_style),
|
||||
Span::raw(format!(" / 12")),
|
||||
Span::raw(format!(" / {}", header_lines + 10)),
|
||||
]),
|
||||
]);
|
||||
|
||||
|
||||
let warning = Paragraph::new(warning_text)
|
||||
.alignment(Alignment::Center)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("UI Too Small (Q to Quit)"),
|
||||
.title(format!("UI Too Small [{}]", q_informer)),
|
||||
);
|
||||
|
||||
too_small = true;
|
||||
|
|
@ -247,7 +346,7 @@ pub async fn run_consent_ui(
|
|||
}
|
||||
|
||||
let consent_block = Paragraph::new(Text::from(text_lines))
|
||||
.block(Block::default().title(format!(" Update Tensamin User Consent [Q to Quit] {}, {}, {}", pp_needed, eula_needed, tos_needed)).borders(Borders::ALL));
|
||||
.block(Block::default().title(format!(" Update Tensamin User Consent [{}] ", q_informer)).borders(Borders::ALL));
|
||||
f.render_widget(consent_block, chunks[0]);
|
||||
|
||||
let downgrade_scenario = tos_needed || pp_needed;
|
||||
|
|
@ -258,6 +357,7 @@ pub async fn run_consent_ui(
|
|||
(eula, tos && pp),
|
||||
update_needed,
|
||||
downgrade_scenario,
|
||||
pp_needed || tos_needed
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
|
@ -271,10 +371,19 @@ pub async fn run_consent_ui(
|
|||
continue;
|
||||
}
|
||||
}
|
||||
let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel];
|
||||
let mut possible_states = vec![Focus::Eula];
|
||||
|
||||
if tos_needed {
|
||||
possible_states.push(Focus::Tos);
|
||||
}
|
||||
if pp_needed {
|
||||
possible_states.push(Focus::Pp);
|
||||
}
|
||||
|
||||
possible_states.push(Focus::Cancel);
|
||||
if eula {
|
||||
possible_states.push(Focus::Continue);
|
||||
if tos && pp {
|
||||
if tos && pp && (pp_needed || tos_needed) {
|
||||
possible_states.push(Focus::ContinueAll);
|
||||
}
|
||||
}
|
||||
|
|
@ -311,9 +420,9 @@ pub async fn run_consent_ui(
|
|||
_ => continue,
|
||||
};
|
||||
|
||||
let _ = open::that(get_link(terms_type));
|
||||
let _ = open::that(get_newest_link(terms_type));
|
||||
}
|
||||
KeyCode::Char(' ') => match focus {
|
||||
KeyCode::Char(' ') | KeyCode::Enter => match focus {
|
||||
Focus::Eula => {
|
||||
eula = !eula;
|
||||
tos = !tos_needed;
|
||||
|
|
@ -329,9 +438,6 @@ pub async fn run_consent_ui(
|
|||
pp = !pp
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Enter => match focus {
|
||||
Focus::Cancel => break UserChoice::Deny,
|
||||
Focus::Continue if eula => break UserChoice::AcceptEULA,
|
||||
Focus::ContinueAll if eula && tos && pp => {
|
||||
|
|
@ -351,180 +457,24 @@ pub async fn run_consent_ui(
|
|||
consent.accepted_eula = true;
|
||||
consent.accepted_tos = true;
|
||||
consent.accepted_pp = true;
|
||||
if let Some(_) = eula_for_future {
|
||||
consent.future_eula = eula_for_future;
|
||||
}
|
||||
if let Some(_) = tos_for_future {
|
||||
consent.future_tos = tos_for_future;
|
||||
}
|
||||
if let Some(_) = pp_for_future {
|
||||
consent.future_privacy = pp_for_future;
|
||||
}
|
||||
}
|
||||
UserChoice::AcceptEULA => {
|
||||
consent.accepted_eula = true;
|
||||
if let Some(_) = eula_for_future {
|
||||
consent.future_eula = eula_for_future;
|
||||
}
|
||||
}
|
||||
UserChoice::Deny => {}
|
||||
}
|
||||
|
||||
consent
|
||||
}
|
||||
|
||||
#[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,
|
||||
current_focus: Focus,
|
||||
state: (bool, bool),
|
||||
update_needed: bool,
|
||||
downgrade_scenario: bool,
|
||||
) {
|
||||
let cancel_text = if update_needed {
|
||||
"[Q] Quit"
|
||||
} else {
|
||||
"[Q] Not now"
|
||||
};
|
||||
let continue_text = if downgrade_scenario {
|
||||
"Downgrade"
|
||||
} else {
|
||||
"Continue"
|
||||
};
|
||||
let buttons = vec![
|
||||
(cancel_text, Focus::Cancel),
|
||||
(continue_text, 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 is_focused = current_focus == *focus;
|
||||
|
||||
let style = match focus {
|
||||
Focus::Cancel => {
|
||||
if is_focused {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::Red)
|
||||
}
|
||||
}
|
||||
|
||||
Focus::Continue => {
|
||||
if is_focused && state.0 {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if state.0 {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
}
|
||||
}
|
||||
|
||||
Focus::ContinueAll => {
|
||||
if is_focused && state.1 {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if state.1 {
|
||||
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