1use crate::IoResult;
4use build_html::{Container, ContainerType, Html, HtmlContainer, HtmlElement, HtmlPage, HtmlTag};
5use bytes::Bytes;
6use colour::{green, green_ln_bold, red};
7use log::{error, warn};
8use reqwest::{Client, Response};
9use std::{
10 error::Error as ErrorTrait,
11 fs::{write, File},
12 io::{self, Error, ErrorKind, Read, Write},
13 process,
14 slice::Iter,
15};
16use tokio::runtime::Runtime;
17use webbrowser;
18
19#[derive(Debug, Clone)]
21pub struct Github<'a> {
22 owner: &'a str,
23 repo: &'a str,
24 branch: &'a str,
25 path: &'a str,
26 file: &'a str,
27 url: String,
28}
29
30impl<'a> Github<'a> {
31 pub fn new(
33 owner: &'a str,
34 repo: &'a str,
35 branch: &'a str,
36 path: &'a str,
37 file: &'a str,
38 ) -> Github<'a> {
39 Github {
40 owner,
41 repo,
42 branch,
43 path,
44 file,
45 url: String::new(),
46 }
47 }
48
49 pub fn get_url(&self) -> String {
51 self.url.clone()
52 }
53
54 pub fn build_url(&mut self) {
67 let url = format!(
68 "https://raw.githubusercontent.com/{}/{}/refs/heads/{}/{}/{}",
69 self.owner, self.repo, self.branch, self.path, self.file
70 );
71 self.url = url;
72 }
73
74 pub fn build_image_url(&mut self) {
87 let url = format!(
88 "https://github.com/{}/{}/blob/{}/{}/{}",
89 self.owner, self.repo, self.branch, self.path, self.file
90 );
91 self.url = url;
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct GHResponse {
98 raw_url: Vec<String>,
99 image_url: Vec<Bytes>,
100}
101
102impl GHResponse {
103 pub fn new(raw_url: Vec<String>, image_url: Vec<Bytes>) -> GHResponse {
105 GHResponse { raw_url, image_url }
106 }
107}
108
109pub async fn send_request(url: &String) -> Response {
119 let client = Client::new();
121
122 let request = client.get(url).send().await;
124
125 match request {
127 Ok(response) => response,
128 Err(e) => {
129 error!("error sending get request for url: {url}\n{e}");
130 process::exit(1);
131 }
132 }
133}
134
135pub async fn handle_response(urls: Vec<String>) -> Vec<String> {
140 let mut text_vec: Vec<String> = Vec::new();
141
142 for url in urls {
144 let response = send_request(&url).await;
145
146 let text = response.text().await;
147
148 match text {
150 Ok(result) => text_vec.push(result),
151 Err(e) => {
152 error!("error retrieving full response text: {e}");
153 process::exit(1);
154 }
155 }
156 }
157
158 text_vec
159}
160
161pub async fn handle_image_response(urls: Vec<String>) -> Vec<Bytes> {
166 let mut blob_vec: Vec<Bytes> = Vec::new();
167
168 for url in urls {
170 let response = send_request(&url).await;
171
172 let blob = response.bytes().await;
173
174 match blob {
176 Ok(result) => blob_vec.push(result),
177 Err(e) => {
178 error!("error retrieving response bytes: {e}");
179 process::exit(1);
180 }
181 }
182 }
183
184 blob_vec
185}
186
187pub fn run_async_functions(
191 urls1: Vec<String>,
192 urls2: Vec<String>,
193) -> Result<GHResponse, Box<dyn ErrorTrait>> {
194 let rt = Runtime::new()?;
195
196 let result1 = rt.block_on(handle_response(urls1));
198 let result2 = rt.block_on(handle_image_response(urls2));
199
200 Ok(GHResponse::new(result1, result2))
201}
202
203pub fn generate_website_files(boolean: bool) -> Result<(), Box<dyn ErrorTrait>> {
210 let mut github_main_css = Github::new(
212 "OneilNvM",
213 "rl-hours-tracker",
214 "master",
215 "website/css",
216 "main.css",
217 );
218 let mut github_home_css = Github::new(
219 "OneilNvM",
220 "rl-hours-tracker",
221 "master",
222 "website/css",
223 "home.css",
224 );
225 let mut github_animations_js = Github::new(
226 "OneilNvM",
227 "rl-hours-tracker",
228 "master",
229 "website/js",
230 "animations.js",
231 );
232 let mut github_grey_icon = Github::new(
233 "OneilNvM",
234 "rl-hours-tracker",
235 "master",
236 "website/images",
237 "rl-icon-grey.png",
238 );
239 let mut github_white_icon = Github::new(
240 "OneilNvM",
241 "rl-hours-tracker",
242 "master",
243 "website/images",
244 "rl-icon-white.png",
245 );
246
247 github_main_css.build_url();
249 github_home_css.build_url();
250 github_animations_js.build_url();
251 github_grey_icon.build_url();
252 github_white_icon.build_url();
253
254 let github_text_vec = vec![
255 github_main_css.url,
256 github_home_css.url,
257 github_animations_js.url,
258 ];
259
260 let github_blob_vec = vec![github_grey_icon.url, github_white_icon.url];
261
262 let ghresponse = run_async_functions(github_text_vec, github_blob_vec)?;
264
265 let mut bytes_iter = ghresponse.image_url.iter();
266 let mut raw_iter = ghresponse.raw_url.iter();
267
268 write(
270 "C:\\RLHoursFolder\\website\\images\\rl-icon-grey.png",
271 bytes_iter.next().unwrap(),
272 )
273 .unwrap_or_else(|e| warn!("failed to write rl-icon-grey.png: {e}"));
274 write(
275 "C:\\RLHoursFolder\\website\\images\\rl-icon-white.png",
276 bytes_iter.next().unwrap(),
277 )
278 .unwrap_or_else(|e| warn!("failed to write rl-icon-white.png: {e}"));
279
280 create_website_files(&mut raw_iter, boolean)
282}
283
284fn create_website_files(
285 raw_iter: &mut Iter<'_, String>,
286 boolean: bool,
287) -> Result<(), Box<dyn ErrorTrait>> {
288 let mut index = File::create("C:\\RLHoursFolder\\website\\pages\\index.html")?;
290 let main_styles = File::create("C:\\RLHoursFolder\\website\\css\\main.css");
291 let home_styles = File::create("C:\\RLHoursFolder\\website\\css\\home.css");
292 let animations_js = File::create("C:\\RLHoursFolder\\website\\js\\animations.js");
293 let mut hours_file = File::open("C:\\RLHoursFolder\\hours.txt");
294 let mut date_file = File::open("C:\\RLHoursFolder\\date.txt");
295
296 match main_styles {
298 Ok(mut ms_file) => {
299 match ms_file.write_all(raw_iter.next().unwrap().as_bytes()) {
301 Ok(_) => (),
302 Err(e) => warn!("failed to write to main.css: {e}"),
303 }
304 }
305 Err(e) => warn!("failed to create main.css: {e}"),
306 }
307
308 match home_styles {
310 Ok(mut hs_file) => {
311 match hs_file.write_all(raw_iter.next().unwrap().as_bytes()) {
313 Ok(_) => (),
314 Err(e) => warn!("failed to write to home.css: {e}"),
315 }
316 }
317 Err(e) => warn!("failed to create home.css: {e}"),
318 }
319
320 match animations_js {
322 Ok(mut a_js_file) => {
323 match a_js_file.write_all(raw_iter.next().unwrap().as_bytes()) {
325 Ok(_) => (),
326 Err(e) => warn!("failed to write to animations.js: {e}"),
327 }
328 }
329 Err(e) => warn!("failed to create animations.js: {e}"),
330 }
331
332 let contents: String = generate_page(&mut hours_file, &mut date_file)?;
334
335 let page = contents.replace("<body>", "<body class=\"body adaptive\">");
337
338 index.write_all(page.as_bytes())?;
340
341 if boolean {
343 let mut option = String::new();
344
345 print!("Open hours website in browser (");
346 green!("y");
347 print!(" / ");
348 red!("n");
349 print!("): ");
350 io::stdout()
351 .flush()
352 .unwrap_or_else(|_| println!("Open hours website in browser (y/n)?"));
353 io::stdin().read_line(&mut option).unwrap();
354
355 if option.trim().to_lowercase() == "y"
356 && webbrowser::open("C:\\RLHoursFolder\\website\\pages\\index.html").is_ok()
357 {
358 green_ln_bold!("OK\n");
359 }
360 }
361
362 Ok(())
363}
364
365fn generate_page(
372 hours_file: &mut IoResult<File>,
373 date_file: &mut IoResult<File>,
374) -> IoResult<String> {
375 let mut page = HtmlPage::new()
376 .with_title("Rocket League Hours Tracker")
377 .with_meta(vec![("charset", "UTF-8")])
378 .with_meta(vec![("name", "viewport"), ("content", "width=device-width, initial-scale=1.0")])
379 .with_head_link("../css/main.css", "stylesheet")
380 .with_head_link("../css/home.css", "stylesheet")
381 .with_head_link("https://fonts.googleapis.com", "preconnect")
382 .with_head_link_attr("https://fonts.gstatic.com", "preconnect", [("crossorigin", "")])
383 .with_head_link("https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap", "stylesheet")
384 .with_head_link("https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Oswald:wght@200..700&display=swap", "stylesheet")
385 .with_script_link("../js/animations.js");
386
387 page.add_container(
388 Container::new(ContainerType::Div)
389 .with_attributes(vec![("class", "animation-div adaptive")])
390 .with_raw(""),
391 );
392
393 let mut hrs_content = String::new();
394 let mut date_content = String::new();
395
396 if let Ok(ref mut hrs_file) = hours_file {
397 if hrs_file.read_to_string(&mut hrs_content).is_err() {
398 return Err(Error::new(
399 ErrorKind::InvalidData,
400 "The files contents are not valid UTF-8.",
401 ));
402 }
403 } else {
404 return Err(Error::new(ErrorKind::NotFound, "The file 'hours.txt' could not be opened. Either it does not exist or it is not in the 'RLHoursFolder' directory."));
405 }
406
407 if let Ok(ref mut dt_file) = date_file {
408 if dt_file.read_to_string(&mut date_content).is_err() {
409 return Err(Error::new(
410 ErrorKind::InvalidData,
411 "The files contents are not valid UTF-8.",
412 ));
413 }
414 } else {
415 return Err(Error::new(ErrorKind::NotFound, "The file 'hours.txt' could not be opened. Either it does not exist or it is not in the 'RLHoursFolder' directory."));
416 }
417
418 let mut hrs_lines: Vec<&str> = hrs_content.split("\n").collect();
419 let mut date_lines: Vec<&str> = date_content.split("\n").collect();
420
421 hrs_lines.pop();
422 date_lines.pop();
423
424 let main_heading_vec: Vec<&str> = hrs_lines.remove(0).split_whitespace().collect();
425
426 let main_heading = format!(
427 "{} {}<br>{} Tracker",
428 main_heading_vec[0], main_heading_vec[1], main_heading_vec[2]
429 );
430
431 page.add_container(
432 Container::new(ContainerType::Header)
433 .with_attributes(vec![("class", "header")])
434 .with_container(Container::new(ContainerType::Div).with_header_attr(
435 1,
436 main_heading,
437 vec![("class", "main-title bebas-neue-regular")],
438 )),
439 );
440
441 let nav_container = HtmlElement::new(HtmlTag::Div)
442 .with_attribute("class", "nav-container flex-column")
443 .with_container(
444 Container::new(ContainerType::Div)
445 .with_attributes(vec![("class", "your-hours-div nav-div")])
446 .with_link("#hours", "Your Hours"),
447 )
448 .with_container(
449 Container::new(ContainerType::Div)
450 .with_attributes(vec![("class", "date-and-times-div nav-div")])
451 .with_link("#dates", "Date And Times"),
452 );
453
454 page.add_container(
455 Container::new(ContainerType::Nav)
456 .with_attributes(vec![("class", "nav oswald-font-500")])
457 .with_html(nav_container),
458 );
459
460 let mut hours_div =
461 HtmlElement::new(HtmlTag::Div).with_attribute("class", "hours-div flex-column adaptive");
462
463 let mut dates_div = HtmlElement::new(HtmlTag::Div).with_attribute(
464 "class",
465 "dates-div flex-column flex-align-justify-center adaptive",
466 );
467
468 for line in hrs_lines {
469 hours_div.add_paragraph(line);
470 }
471
472 date_lines.reverse();
473
474 let mut counter: usize = 0;
475
476 if date_lines.len() >= 7 {
477 while counter <= 6 {
478 dates_div.add_paragraph(date_lines[counter]);
479
480 counter += 1;
481 }
482 } else {
483 for line in date_lines {
484 dates_div.add_paragraph(line);
485 }
486 }
487
488 let hours_div_container = HtmlElement::new(HtmlTag::Div)
489 .with_attribute("id", "hours")
490 .with_attribute("class", "hours-div-container color flex-column")
491 .with_header(2, "Your Hours Played")
492 .with_html(hours_div);
493
494 let dates_div_container = HtmlElement::new(HtmlTag::Div)
495 .with_attribute("id", "dates")
496 .with_attribute("class", "dates-div-container color flex-column")
497 .with_header(2, "Your time played<br>in the last 7 sessions")
498 .with_html(dates_div);
499
500 page.add_container(
501 Container::new(ContainerType::Main)
502 .with_attributes(vec![("class", "main flex-column color oswald-font-500")])
503 .with_html(hours_div_container)
504 .with_html(dates_div_container),
505 );
506
507 page.add_container(
508 Container::new(ContainerType::Footer)
509 .with_attributes(vec![("class", "footer flex-row oswald-font-700")])
510 .with_paragraph("© OneilNvM 2024 ")
511 .with_link_attr(
512 "https://github.com/OneilNvM/rl-hours-tracker",
513 "Rocket League Hours Tracker Github",
514 [("target", "_blank")],
515 ),
516 );
517
518 Ok(page.to_html_string())
519}