[Add] Markdown viewer & Document loading
Todo: Track accepted Version & Hash of accepted document
This commit is contained in:
parent
4239e903c1
commit
53a057c6e1
8 changed files with 591 additions and 32 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
|
@ -1568,6 +1568,7 @@ dependencies = [
|
||||||
"json",
|
"json",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"open",
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"pnet",
|
"pnet",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
|
@ -1626,6 +1627,25 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
|
@ -2056,6 +2076,17 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
|
||||||
|
dependencies = [
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
"pathdiff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.75"
|
version = "0.10.75"
|
||||||
|
|
@ -2144,6 +2175,12 @@ version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pbkdf2"
|
name = "pbkdf2"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
|
|
|
||||||
|
|
@ -58,3 +58,4 @@ strum = "0.27.2"
|
||||||
strum_macros = "0.27.2"
|
strum_macros = "0.27.2"
|
||||||
ratatui = "0.30.0"
|
ratatui = "0.30.0"
|
||||||
ratatui_input = "0.1.3"
|
ratatui_input = "0.1.3"
|
||||||
|
open = "5.3.3"
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
pub mod terms_checker;
|
|
||||||
|
|
@ -9,17 +9,16 @@ use tokio::time::{Duration, sleep};
|
||||||
mod auth;
|
mod auth;
|
||||||
mod communities;
|
mod communities;
|
||||||
mod data;
|
mod data;
|
||||||
mod eula;
|
|
||||||
mod gui;
|
mod gui;
|
||||||
mod langu;
|
mod langu;
|
||||||
mod omikron;
|
mod omikron;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod terms;
|
||||||
mod users;
|
mod users;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use crate::communities::community_manager;
|
use crate::communities::community_manager;
|
||||||
use crate::communities::interactables::registry;
|
use crate::communities::interactables::registry;
|
||||||
use crate::eula::terms_checker;
|
|
||||||
use crate::gui::app_state::AppState;
|
use crate::gui::app_state::AppState;
|
||||||
use crate::gui::input_handler;
|
use crate::gui::input_handler;
|
||||||
use crate::gui::log_panel;
|
use crate::gui::log_panel;
|
||||||
|
|
@ -29,6 +28,7 @@ use crate::langu::language_creator;
|
||||||
use crate::langu::language_manager::format;
|
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::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;
|
||||||
|
|
@ -48,7 +48,7 @@ async fn main() {
|
||||||
*SHUTDOWN.write().await = false;
|
*SHUTDOWN.write().await = false;
|
||||||
|
|
||||||
// EULA
|
// EULA
|
||||||
let (tos, pp) = terms_checker::ConsentManager::check();
|
let (tos, pp) = terms_checker::ConsentManager::check().await;
|
||||||
if !tos {
|
if !tos {
|
||||||
println!("You need to accept our End User Licence Agreement before launching!");
|
println!("You need to accept our End User Licence Agreement before launching!");
|
||||||
println!("You can find this at 'agreements'!");
|
println!("You can find this at 'agreements'!");
|
||||||
|
|
@ -58,6 +58,7 @@ async fn main() {
|
||||||
println!(
|
println!(
|
||||||
"Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!"
|
"Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!"
|
||||||
);
|
);
|
||||||
|
println!("In future releases this will be optional!");
|
||||||
println!("You can find this at 'agreements'!");
|
println!("You can find this at 'agreements'!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
427
src/terms/md_viewer.rs
Normal file
427
src/terms/md_viewer.rs
Normal file
|
|
@ -0,0 +1,427 @@
|
||||||
|
use crossterm::event::{self, Event, KeyCode};
|
||||||
|
use ratatui::{
|
||||||
|
DefaultTerminal,
|
||||||
|
prelude::*,
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FileViewer {
|
||||||
|
title: String,
|
||||||
|
text: Vec<DisplayLine>,
|
||||||
|
scroll: u16,
|
||||||
|
scroll_x: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileViewer {
|
||||||
|
pub fn new(title: String, content: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
title,
|
||||||
|
text: parse_document(content.to_owned()),
|
||||||
|
scroll: 0,
|
||||||
|
scroll_x: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn force_popup(mut self, mut terminal: DefaultTerminal) -> DefaultTerminal {
|
||||||
|
loop {
|
||||||
|
terminal
|
||||||
|
.draw(|f| {
|
||||||
|
let area = f.area();
|
||||||
|
self.draw(f, area);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if event::poll(Duration::from_millis(100)).unwrap() {
|
||||||
|
let ev = event::read().unwrap();
|
||||||
|
self.handle_event(&ev);
|
||||||
|
|
||||||
|
if matches!(ev, Event::Key(k) if k.code == KeyCode::Char('q')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminal
|
||||||
|
}
|
||||||
|
fn draw(&self, f: &mut Frame, area: Rect) {
|
||||||
|
use ratatui::text::Text;
|
||||||
|
|
||||||
|
let mut rendered_lines = Vec::new();
|
||||||
|
|
||||||
|
for display_line in &self.text {
|
||||||
|
if display_line.scrollable {
|
||||||
|
let content: String = display_line
|
||||||
|
.line
|
||||||
|
.spans
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.content.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let start = self.scroll_x as usize;
|
||||||
|
let width = area.width as usize - 2;
|
||||||
|
|
||||||
|
let visible = if start < content.chars().count() {
|
||||||
|
content.chars().skip(start).take(width).collect()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut chars: Vec<char> = visible.chars().collect();
|
||||||
|
|
||||||
|
if start > 0 && !chars.is_empty() {
|
||||||
|
chars[0] = '<';
|
||||||
|
}
|
||||||
|
|
||||||
|
if start + width < content.chars().count() && !chars.is_empty() {
|
||||||
|
let last = chars.len() - 1;
|
||||||
|
chars[last] = '>';
|
||||||
|
}
|
||||||
|
|
||||||
|
let visible: String = chars.into_iter().collect();
|
||||||
|
|
||||||
|
rendered_lines.push(Line::from(Span::styled(
|
||||||
|
visible,
|
||||||
|
display_line
|
||||||
|
.line
|
||||||
|
.spans
|
||||||
|
.first()
|
||||||
|
.map(|s| s.style)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
rendered_lines.push(display_line.line.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(Text::from(rendered_lines))
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(self.title.as_str()),
|
||||||
|
)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.scroll((self.scroll, 0));
|
||||||
|
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_event(&mut self, event: &Event) {
|
||||||
|
if let Event::Key(key) = event {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Down => self.scroll = self.scroll.saturating_add(1),
|
||||||
|
KeyCode::Up => self.scroll = self.scroll.saturating_sub(1),
|
||||||
|
KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10),
|
||||||
|
KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10),
|
||||||
|
KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2),
|
||||||
|
KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn parse_document(input: String) -> Vec<DisplayLine> {
|
||||||
|
let mut lines_vec = Vec::new();
|
||||||
|
let mut in_code_block = false;
|
||||||
|
let liness: Vec<String> = input.lines().map(String::from).collect();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < liness.len() {
|
||||||
|
let raw = &liness[i];
|
||||||
|
|
||||||
|
if raw.trim().starts_with("```") {
|
||||||
|
in_code_block = !in_code_block;
|
||||||
|
let code: String = if raw.trim().replace("```", "").is_empty() {
|
||||||
|
"──".to_string()
|
||||||
|
} else {
|
||||||
|
raw.trim().replace("```", "")
|
||||||
|
};
|
||||||
|
lines_vec.push(DisplayLine {
|
||||||
|
line: Line::from(Span::styled(
|
||||||
|
format!("────────{}────────", code),
|
||||||
|
Style::default().fg(Color::DarkGray),
|
||||||
|
)),
|
||||||
|
scrollable: false,
|
||||||
|
});
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_code_block {
|
||||||
|
lines_vec.push(DisplayLine {
|
||||||
|
line: Line::from(Span::styled(
|
||||||
|
raw.to_string(),
|
||||||
|
Style::default().fg(Color::Yellow),
|
||||||
|
)),
|
||||||
|
scrollable: false,
|
||||||
|
});
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if raw.starts_with("### ") {
|
||||||
|
lines_vec.push(DisplayLine {
|
||||||
|
line: Line::from(Span::styled(
|
||||||
|
raw.trim_start_matches("### ").to_string(),
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)),
|
||||||
|
scrollable: false,
|
||||||
|
});
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if raw.starts_with("## ") {
|
||||||
|
lines_vec.push(DisplayLine {
|
||||||
|
line: Line::from(Span::styled(
|
||||||
|
raw.trim_start_matches("## ").to_string(),
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightCyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)),
|
||||||
|
scrollable: false,
|
||||||
|
});
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if raw.starts_with("# ") {
|
||||||
|
lines_vec.push(DisplayLine {
|
||||||
|
line: Line::from(Span::styled(
|
||||||
|
raw.trim_start_matches("# ").to_string(),
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Gray)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)),
|
||||||
|
scrollable: false,
|
||||||
|
});
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw.trim_start().starts_with("- ") {
|
||||||
|
let indent = raw.chars().take_while(|c| *c == ' ').count();
|
||||||
|
lines_vec.push(DisplayLine {
|
||||||
|
line: Line::from(Span::raw(format!(
|
||||||
|
"{}• {}",
|
||||||
|
" ".repeat(indent),
|
||||||
|
raw.trim_start_matches("- ")
|
||||||
|
))),
|
||||||
|
scrollable: false,
|
||||||
|
});
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw.trim().starts_with('|') && raw.contains('|') {
|
||||||
|
let mut table_lines = vec![raw.clone()];
|
||||||
|
let mut j = i + 1;
|
||||||
|
while j < liness.len() && liness[j].trim().starts_with('|') {
|
||||||
|
table_lines.push(liness[j].clone());
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let table = parse_table(&table_lines.iter().map(|s| s.as_str()).collect::<Vec<_>>());
|
||||||
|
lines_vec.extend(table_to_lines(table));
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines_vec.push(DisplayLine {
|
||||||
|
line: Line::from(parse_inline(raw.as_str())),
|
||||||
|
scrollable: false,
|
||||||
|
});
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines_vec
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_inline(input: &str) -> Vec<Span<'static>> {
|
||||||
|
let mut spans = Vec::new();
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
let mut bold = false;
|
||||||
|
let mut underline = false;
|
||||||
|
let mut code = false;
|
||||||
|
|
||||||
|
let mut chars = input.chars().peekable();
|
||||||
|
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
let toggle = match c {
|
||||||
|
'*' if chars.peek() == Some(&'*') => {
|
||||||
|
chars.next();
|
||||||
|
Some("bold")
|
||||||
|
}
|
||||||
|
'_' if chars.peek() == Some(&'_') => {
|
||||||
|
chars.next();
|
||||||
|
Some("underline")
|
||||||
|
}
|
||||||
|
'`' => Some("code"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(kind) = toggle {
|
||||||
|
flush_span(&mut spans, &mut buf, current_style(bold, underline, code));
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
"bold" => bold = !bold,
|
||||||
|
"underline" => underline = !underline,
|
||||||
|
"code" => code = !code,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
flush_span(&mut spans, &mut buf, current_style(bold, underline, code));
|
||||||
|
spans
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_style(bold: bool, underline: bool, code: bool) -> Style {
|
||||||
|
let mut style = Style::default();
|
||||||
|
|
||||||
|
if bold {
|
||||||
|
style = style.add_modifier(Modifier::BOLD);
|
||||||
|
}
|
||||||
|
if underline {
|
||||||
|
style = style.add_modifier(Modifier::UNDERLINED);
|
||||||
|
}
|
||||||
|
if code {
|
||||||
|
style = style.fg(Color::Yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
style
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DisplayLine {
|
||||||
|
line: Line<'static>,
|
||||||
|
scrollable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_to_lines(table: Vec<Vec<String>>) -> Vec<DisplayLine> {
|
||||||
|
if table.len() < 2 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = &table[0];
|
||||||
|
|
||||||
|
let widths: Vec<usize> = header
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, h)| {
|
||||||
|
let h_len = h.chars().count().max(1);
|
||||||
|
if i == 0 {
|
||||||
|
table
|
||||||
|
.iter()
|
||||||
|
.map(|row| row.get(i).map(|c| c.chars().count()).unwrap_or(0))
|
||||||
|
.max()
|
||||||
|
.unwrap_or(h_len)
|
||||||
|
} else {
|
||||||
|
let max = (3 * h_len) as usize;
|
||||||
|
max.max(h_len)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
|
for (row_idx, row) in table.iter().enumerate() {
|
||||||
|
if row_idx == 1 {
|
||||||
|
let divider = widths
|
||||||
|
.iter()
|
||||||
|
.map(|w| "─".repeat(*w))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("─┼─");
|
||||||
|
|
||||||
|
lines.push(DisplayLine {
|
||||||
|
line: Line::from(Span::styled(divider, Style::default().fg(Color::DarkGray))),
|
||||||
|
scrollable: true,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let wrapped_cells: Vec<Vec<String>> = row
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, cell)| wrap_cell(cell, widths[i]))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let row_height = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1);
|
||||||
|
|
||||||
|
for line_idx in 0..row_height {
|
||||||
|
let mut line = String::new();
|
||||||
|
|
||||||
|
for (i, cell) in wrapped_cells.iter().enumerate() {
|
||||||
|
let content = cell.get(line_idx).map(String::as_str).unwrap_or("");
|
||||||
|
line.push_str(&format!("{:width$}", content, width = widths[i]));
|
||||||
|
if i < wrapped_cells.len() - 1 {
|
||||||
|
line.push_str(" │ ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let style = if row_idx == 0 {
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Green)
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.push(DisplayLine {
|
||||||
|
line: Line::from(Span::styled(line, style)),
|
||||||
|
scrollable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
fn wrap_cell(cell: &str, width: usize) -> Vec<String> {
|
||||||
|
if width == 0 {
|
||||||
|
return vec![String::new()];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut chars = cell.chars();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let line: String = chars.by_ref().take(width).collect();
|
||||||
|
if line.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.is_empty() {
|
||||||
|
out.push(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_span(spans: &mut Vec<Span>, buf: &mut String, style: Style) {
|
||||||
|
if !buf.is_empty() {
|
||||||
|
spans.push(Span::styled(buf.clone(), style));
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_table(lines: &[&str]) -> Vec<Vec<String>> {
|
||||||
|
let mut table = Vec::new();
|
||||||
|
|
||||||
|
for &line in lines {
|
||||||
|
if !line.starts_with('|') || !line.contains('|') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let row: Vec<String> = line
|
||||||
|
.trim_matches('|')
|
||||||
|
.split('|')
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.collect();
|
||||||
|
table.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
3
src/terms/mod.rs
Normal file
3
src/terms/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod md_viewer;
|
||||||
|
pub mod terms_checker;
|
||||||
|
pub mod terms_getter;
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
use crate::util::file_util::{load_file, save_file};
|
use crate::{
|
||||||
|
terms::{
|
||||||
|
md_viewer::FileViewer,
|
||||||
|
terms_getter::{Type, get_link, get_terms},
|
||||||
|
},
|
||||||
|
util::file_util::{load_file, save_file},
|
||||||
|
};
|
||||||
use crossterm::event::{self, Event, KeyCode};
|
use crossterm::event::{self, Event, KeyCode};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
|
@ -11,14 +17,14 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
pub struct ConsentManager;
|
pub struct ConsentManager;
|
||||||
impl ConsentManager {
|
impl ConsentManager {
|
||||||
pub fn check() -> (bool, bool) {
|
pub async fn check() -> (bool, bool) {
|
||||||
let file = load_file("", "agreements");
|
let file = load_file("", "agreements");
|
||||||
let existing = ConsentUiState::from_str(&file).sanitize();
|
let existing = ConsentUiState::from_str(&file).sanitize();
|
||||||
|
|
||||||
let final_state = if existing.eula {
|
let final_state = if existing.eula {
|
||||||
existing
|
existing
|
||||||
} else {
|
} else {
|
||||||
let choice = run_consent_ui();
|
let choice = run_consent_ui().await;
|
||||||
let state = match choice {
|
let state = match choice {
|
||||||
UserChoice::Deny => ConsentUiState::denied(),
|
UserChoice::Deny => ConsentUiState::denied(),
|
||||||
UserChoice::AcceptEULA => ConsentUiState {
|
UserChoice::AcceptEULA => ConsentUiState {
|
||||||
|
|
@ -92,11 +98,11 @@ impl ConsentUiState {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs();
|
.as_secs();
|
||||||
format!(
|
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/\
|
"\"EULA=true\" indicates that you read and accepted the End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\
|
||||||
\nEULA={}\
|
\nEULA={}\
|
||||||
\n\"PrivacyPolicy=true\" indicates that you read and accepted the Privacy Policy. You can find our Privacy Policy at https://docs.tensamin.net/legal/privacy-policy/\
|
\n\"PrivacyPolicy=true\" indicates that you read and accepted the Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy-policy/\
|
||||||
\nPrivacyPolicy={}\
|
\nPrivacyPolicy={}\
|
||||||
\n\"ToS=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/\
|
\n\"ToS=true\" indicates that you read and accepted the Terms of Service. You can find our Terms of Service at https://legal.tensamin.net/terms-of-service/\
|
||||||
\nToS={}\
|
\nToS={}\
|
||||||
\nThis file reflects the current consent state used by the application.\
|
\nThis file reflects the current consent state used by the application.\
|
||||||
\nIt may be regenerated or overwritten by the application.\
|
\nIt may be regenerated or overwritten by the application.\
|
||||||
|
|
@ -165,7 +171,7 @@ impl ConsentUiState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_consent_ui() -> UserChoice {
|
async fn run_consent_ui() -> UserChoice {
|
||||||
let mut terminal = ratatui::init();
|
let mut terminal = ratatui::init();
|
||||||
|
|
||||||
let mut state = ConsentUiState {
|
let mut state = ConsentUiState {
|
||||||
|
|
@ -176,6 +182,7 @@ fn run_consent_ui() -> UserChoice {
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = loop {
|
let result = loop {
|
||||||
|
let mut too_small = false;
|
||||||
terminal
|
terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
let mut needed_height = 5;
|
let mut needed_height = 5;
|
||||||
|
|
@ -206,47 +213,52 @@ fn run_consent_ui() -> UserChoice {
|
||||||
width: content_width,
|
width: content_width,
|
||||||
height: content_height,
|
height: content_height,
|
||||||
});
|
});
|
||||||
let eula_text = if size.width < 76 {
|
let eula_text = if size.width < 70 {
|
||||||
"EULA ¹ (https://docs.tensamin.net/legal/eula/)"
|
"EULA ¹ (https://legal.tensamin.net/eula/)"
|
||||||
} else {
|
} else {
|
||||||
"End User Licence Agreement ¹ (https://docs.tensamin.net/legal/eula/)"
|
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)"
|
||||||
};
|
};
|
||||||
let tos_text = if size.width < 76 {
|
let tos_text = if size.width < 72 {
|
||||||
"ToS ² (https://docs.tensamin.net/legal/terms-of-service/)"
|
"ToS ² (https://legal.tensamin.net/terms-of-service/)"
|
||||||
} else {
|
} else {
|
||||||
"Terms of Service ² (https://docs.tensamin.net/legal/terms-of-service/)"
|
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)"
|
||||||
};
|
};
|
||||||
let pp_text = if size.width < 76 {
|
let pp_text = if size.width < 68 {
|
||||||
"PP ² (https://docs.tensamin.net/legal/privacy-policy/)"
|
"PP ² (https://legal.tensamin.net/privacy-policy/)"
|
||||||
} else {
|
} else {
|
||||||
"Privacy Policy ² (https://docs.tensamin.net/legal/privacy-policy/)"
|
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)"
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) =
|
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) =
|
||||||
if size.width > 132 {
|
if size.width > 132 {
|
||||||
(
|
(
|
||||||
vec![7, 3, 4],
|
vec![7, 3, 7, 4],
|
||||||
vec![
|
vec![
|
||||||
"",
|
"",
|
||||||
"By selecting Continue, you confirm that you have read and agree to the End User License Agreement and applicable Terms of Service.",
|
"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.",
|
"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 > 68 {
|
} else if size.width > 68 {
|
||||||
(
|
(
|
||||||
vec![8, 3, 4],
|
vec![8, 3, 8, 4],
|
||||||
vec![
|
vec![
|
||||||
"",
|
"",
|
||||||
"By selecting Continue, you confirm that you have read and agree",
|
"By selecting Continue, you confirm that you have read and agree",
|
||||||
"to the End User License Agreement and applicable Terms of Service.",
|
"to the End User License Agreement and applicable Terms of Service.",
|
||||||
"",
|
"",
|
||||||
"Tensamin services require acceptance of the ToS and Privacy Policy.",
|
"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 {
|
} else {
|
||||||
(
|
(
|
||||||
vec![9, 3, 4],
|
vec![9, 3, 10, 4],
|
||||||
vec![
|
vec![
|
||||||
"",
|
"",
|
||||||
"By selecting Continue, you confirm that you have",
|
"By selecting Continue, you confirm that you have",
|
||||||
|
|
@ -254,7 +266,10 @@ fn run_consent_ui() -> UserChoice {
|
||||||
"and applicable Terms of Service.",
|
"and applicable Terms of Service.",
|
||||||
"",
|
"",
|
||||||
"Tensamin services require acceptance of the",
|
"Tensamin services require acceptance of the",
|
||||||
"Terms of Service and Privacy Policy."
|
"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.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -281,33 +296,33 @@ fn run_consent_ui() -> UserChoice {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if size.width < 63 || size.height < needed_height as u16 {
|
if size.width < 60 || size.height < needed_height as u16 {
|
||||||
let width_style = if size.width > 76 {
|
let width_style = if size.width > 76 {
|
||||||
Style::default().fg(Color::Green)
|
Style::default().fg(Color::Green)
|
||||||
} else if size.width >= 63 {
|
} else if size.width >= 60 {
|
||||||
Style::default().fg(Color::Yellow)
|
Style::default().fg(Color::Yellow)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::Red)
|
Style::default().fg(Color::Red)
|
||||||
};
|
};
|
||||||
|
|
||||||
let height_style = if size.height < needed_height as u16 {
|
let height_style = if size.height < 12 {
|
||||||
Style::default().fg(Color::Red)
|
Style::default().fg(Color::Red)
|
||||||
} else if size.height < 17 {
|
} else if size.height > 19 {
|
||||||
Style::default().fg(Color::Yellow)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::Green)
|
Style::default().fg(Color::Green)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
};
|
};
|
||||||
|
|
||||||
let warning_text = Text::from(vec![
|
let warning_text = Text::from(vec![
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw("Width: "),
|
Span::raw("Width: "),
|
||||||
Span::styled(format!("{}", size.width), width_style),
|
Span::styled(format!("{}", size.width), width_style),
|
||||||
Span::raw(" / 63"),
|
Span::raw(" / 60"),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw("Height: "),
|
Span::raw("Height: "),
|
||||||
Span::styled(format!("{}", size.height), height_style),
|
Span::styled(format!("{}", size.height), height_style),
|
||||||
Span::raw(format!(" / {}", needed_height)),
|
Span::raw(format!(" / 12")),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -319,6 +334,7 @@ fn run_consent_ui() -> UserChoice {
|
||||||
.title("UI Too Small (Q to Quit)"),
|
.title("UI Too Small (Q to Quit)"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
too_small = true;
|
||||||
f.render_widget(warning, size);
|
f.render_widget(warning, size);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -333,11 +349,47 @@ fn run_consent_ui() -> UserChoice {
|
||||||
|
|
||||||
if event::poll(Duration::from_millis(200)).unwrap() {
|
if event::poll(Duration::from_millis(200)).unwrap() {
|
||||||
if let Event::Key(key) = event::read().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 {
|
match key.code {
|
||||||
KeyCode::Esc => break UserChoice::Deny,
|
KeyCode::Esc => break UserChoice::Deny,
|
||||||
KeyCode::Up => state.prev(),
|
KeyCode::Up => state.prev(),
|
||||||
KeyCode::Down | KeyCode::Tab => state.next(),
|
KeyCode::Down | KeyCode::Tab => state.next(),
|
||||||
KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny,
|
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 {
|
KeyCode::Char(' ') => match state.focus {
|
||||||
Focus::Eula => {
|
Focus::Eula => {
|
||||||
state.eula = !state.eula;
|
state.eula = !state.eula;
|
||||||
39
src/terms/terms_getter.rs
Normal file
39
src/terms/terms_getter.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Type {
|
||||||
|
EULA,
|
||||||
|
TOS,
|
||||||
|
PP,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Type {
|
||||||
|
pub fn to_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::EULA => "eula",
|
||||||
|
Self::TOS => "tos",
|
||||||
|
Self::PP => "pp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::EULA => "End User License Agreement".to_string(),
|
||||||
|
Self::TOS => "Terms of Service".to_string(),
|
||||||
|
Self::PP => "Privacy Policy".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_link(terms_type: Type) -> String {
|
||||||
|
format!("https://legal.tensamin.net/{}/", terms_type.to_str())
|
||||||
|
}
|
||||||
|
pub async fn get_terms(terms_type: Type) -> Option<String> {
|
||||||
|
let body = reqwest::get(format!(
|
||||||
|
"https://legal.tensamin.net/api/text/{}/",
|
||||||
|
terms_type.to_str()
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.ok()?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
Some(body)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue