mirror of
https://github.com/Dangoware/confetti-box.git
synced 2025-04-19 23:32:58 -05:00
Moved time selection buttons mostly to the backend
This commit is contained in:
parent
6c7763d5e6
commit
2f785d78c1
8 changed files with 234 additions and 88 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -996,9 +996,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.88"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
|
||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -1470,9 +1470,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.40.0"
|
||||
version = "1.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
|
||||
checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
|
81
src/main.rs
81
src/main.rs
|
@ -1,5 +1,5 @@
|
|||
mod database;
|
||||
mod time_string;
|
||||
mod strings;
|
||||
mod settings;
|
||||
|
||||
use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}, time::Duration};
|
||||
|
@ -8,12 +8,12 @@ use chrono::{DateTime, Utc};
|
|||
use database::{clean_loop, Database, MochiFile};
|
||||
use log::info;
|
||||
use rocket::{
|
||||
data::{Limits, ToByteUnit}, form::Form, fs::{FileServer, Options, TempFile}, get, post, response::content::{RawCss, RawJavaScript}, routes, serde::{json::Json, Serialize}, tokio::{self, fs::File, io::AsyncReadExt}, Config, FromForm, State
|
||||
data::{Limits, ToByteUnit}, form::Form, fs::{FileServer, Options, TempFile}, get, post, response::content::{RawCss, RawJavaScript}, routes, serde::{json::Json, Serialize}, tokio::{self, fs::File, io::AsyncReadExt}, Config, FromForm, State, http::ContentType,
|
||||
};
|
||||
use settings::Settings;
|
||||
use time_string::parse_time_string;
|
||||
use strings::{parse_time_string, to_pretty_time};
|
||||
use uuid::Uuid;
|
||||
use maud::{html, Markup, DOCTYPE};
|
||||
use maud::{html, Markup, DOCTYPE, PreEscaped};
|
||||
|
||||
fn head(page_title: &str) -> Markup {
|
||||
html! {
|
||||
|
@ -23,6 +23,7 @@ fn head(page_title: &str) -> Markup {
|
|||
title { (page_title) }
|
||||
// Javascript stuff for client side handling
|
||||
script src="request.js" { }
|
||||
link rel="icon" type="image/svg+xml" href="favicon.svg";
|
||||
link rel="stylesheet" href="main.css";
|
||||
}
|
||||
}
|
||||
|
@ -39,35 +40,55 @@ fn form_handler_js() -> RawJavaScript<&'static str> {
|
|||
RawJavaScript(include_str!("static/request.js"))
|
||||
}
|
||||
|
||||
#[get("/favicon.svg")]
|
||||
fn favicon() -> (ContentType, &'static str) {
|
||||
(ContentType::SVG, include_str!("static/favicon.svg"))
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn home() -> Markup {
|
||||
fn home(settings: &State<Settings>) -> Markup {
|
||||
html! {
|
||||
(head("Mochi"))
|
||||
(head("Confetti-Box"))
|
||||
|
||||
center {
|
||||
h1 { "Confetti Box" }
|
||||
h1 { "Confetti-Box 🎉" }
|
||||
h2 { "Files up to " (settings.max_filesize.bytes()) " in size are allowed!" }
|
||||
hr;
|
||||
h3 { "Expire after:" }
|
||||
div id="durationBox" {
|
||||
|
||||
}
|
||||
|
||||
form id="uploadForm" {
|
||||
label for="fileUpload"
|
||||
class="button"
|
||||
onclick="document.getElementById('fileInput').click()"
|
||||
{
|
||||
"Upload File"
|
||||
@for d in &settings.duration.allowed {
|
||||
button.button.{@if settings.duration.default == *d { "selected" }}
|
||||
data-duration-seconds=(d.num_seconds())
|
||||
{
|
||||
(PreEscaped(to_pretty_time(d.num_seconds() as u32)))
|
||||
}
|
||||
}
|
||||
input id="fileInput" type="file" name="fileUpload" onchange="formSubmit(this.parentNode)" style="display:none;";
|
||||
input id="fileDuration" type="text" name="duration" minlength="2" maxlength="7" value="" style="display:none;";
|
||||
}
|
||||
form #uploadForm {
|
||||
// It's stupid how these can't be styled so they're just hidden here...
|
||||
input id="fileInput" type="file" name="fileUpload"
|
||||
onchange="formSubmit(this.parentNode)" data-max-filesize=(settings.max_filesize) style="display:none;";
|
||||
input id="fileDuration" type="text" name="duration" minlength="2"
|
||||
maxlength="7" value=(settings.duration.default) style="display:none;";
|
||||
}
|
||||
button.main_file_upload onclick="document.getElementById('fileInput').click()" {
|
||||
h4 { "Upload File" }
|
||||
p { "Click or Drag and Drop" }
|
||||
}
|
||||
hr;
|
||||
|
||||
h3 { "Uploaded Files" }
|
||||
div #uploadedFilesDisplay {
|
||||
div { p {"File Name Here"} span {" "} div {p {"File Link Here"} button {"copy"}} }
|
||||
}
|
||||
|
||||
div class="progress_box" {
|
||||
progress id="uploadProgress" value="0" max="100" {}
|
||||
p id="uploadProgressValue" class="progress_value" { "" }
|
||||
}
|
||||
|
||||
div id="uploadedFilesDisplay" {
|
||||
h2 { "Uploaded Files" }
|
||||
hr;
|
||||
footer {
|
||||
p {a href="https://github.com/G2-Games/confetti-box" {"Source"}}
|
||||
p {a href="https://g2games.dev/" {"My Website"}}
|
||||
p {a href="#" {"Links"}}
|
||||
p {a href="#" {"Go"}}
|
||||
p {a href="#" {"Here"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,6 +122,14 @@ async fn handle_upload(
|
|||
}))
|
||||
}
|
||||
|
||||
if settings.duration.restrict_to_allowed && !settings.duration.allowed.contains(&t) {
|
||||
return Ok(Json(ClientResponse {
|
||||
status: false,
|
||||
response: "Duration is disallowed",
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
t
|
||||
} else {
|
||||
return Ok(Json(ClientResponse {
|
||||
|
@ -247,7 +276,7 @@ async fn main() {
|
|||
let rocket = rocket::build()
|
||||
.mount(
|
||||
config.server.root_path.clone() + "/",
|
||||
routes![home, handle_upload, form_handler_js, stylesheet, server_info]
|
||||
routes![home, handle_upload, form_handler_js, stylesheet, server_info, favicon]
|
||||
)
|
||||
.mount(
|
||||
config.server.root_path.clone() + "/files",
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::{fs::{self, File}, io::{self, Read, Write}, path::{Path, PathBuf}};
|
|||
use chrono::TimeDelta;
|
||||
use serde_with::serde_as;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::data::ToByteUnit;
|
||||
|
||||
/// A response to the client from the server
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
|
@ -39,9 +40,9 @@ pub struct Settings {
|
|||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_filesize: 128_000_000, // 128 MB
|
||||
duration: DurationSettings::default(),
|
||||
max_filesize: 1.megabytes().into(), // 128 MB
|
||||
overwrite: true,
|
||||
duration: DurationSettings::default(),
|
||||
server: ServerSettings::default(),
|
||||
path: "./settings.toml".into(),
|
||||
database_path: "./database.mochi".into(),
|
||||
|
@ -97,7 +98,7 @@ impl Default for ServerSettings {
|
|||
Self {
|
||||
address: "127.0.0.1".into(),
|
||||
root_path: "/".into(),
|
||||
port: 8955
|
||||
port: 8950
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,10 +116,14 @@ pub struct DurationSettings {
|
|||
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
|
||||
pub default: TimeDelta,
|
||||
|
||||
/// List of allowed durations. An empty list means any are allowed.
|
||||
/// List of recommended lifetimes
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "Vec<serde_with::DurationSeconds<i64>>")]
|
||||
pub allowed: Vec<TimeDelta>,
|
||||
|
||||
/// Restrict the input durations to the allowed ones or not
|
||||
#[serde(default)]
|
||||
pub restrict_to_allowed: bool,
|
||||
}
|
||||
|
||||
impl Default for DurationSettings {
|
||||
|
@ -133,6 +138,7 @@ impl Default for DurationSettings {
|
|||
TimeDelta::days(1),
|
||||
TimeDelta::days(2),
|
||||
],
|
||||
restrict_to_allowed: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
1
src/static/favicon.svg
Normal file
1
src/static/favicon.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#DD2E44" d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"/><path fill="#EA596E" d="M13 12L.416 32.506l-.282.635.011.011c-.208.403.14 1.223.853 1.937.232.232.473.408.709.557L17 17l-4-5z"/><path fill="#A0041E" d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124-1.473 1.474-6.453-1.118-11.126-5.788-4.671-4.672-7.263-9.654-5.79-11.127 1.474-1.473 6.454 1.119 11.127 5.791z"/><path fill="#AA8DD8" d="M18.59 13.609c-.199.161-.459.245-.734.215-.868-.094-1.598-.396-2.109-.873-.541-.505-.808-1.183-.735-1.862.128-1.192 1.324-2.286 3.363-2.066.793.085 1.147-.17 1.159-.292.014-.121-.277-.446-1.07-.532-.868-.094-1.598-.396-2.11-.873-.541-.505-.809-1.183-.735-1.862.13-1.192 1.325-2.286 3.362-2.065.578.062.883-.057 1.012-.134.103-.063.144-.123.148-.158.012-.121-.275-.446-1.07-.532-.549-.06-.947-.552-.886-1.102.059-.549.55-.946 1.101-.886 2.037.219 2.973 1.542 2.844 2.735-.13 1.194-1.325 2.286-3.364 2.067-.578-.063-.88.057-1.01.134-.103.062-.145.123-.149.157-.013.122.276.446 1.071.532 2.037.22 2.973 1.542 2.844 2.735-.129 1.192-1.324 2.286-3.362 2.065-.578-.062-.882.058-1.012.134-.104.064-.144.124-.148.158-.013.121.276.446 1.07.532.548.06.947.553.886 1.102-.028.274-.167.511-.366.671z"/><path fill="#77B255" d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478.324 1.154-.378 2.615-2.35 3.17-.77.216-1.001.584-.97.701.034.118.425.312 1.193.095 1.972-.555 3.333.325 3.657 1.479.326 1.155-.378 2.614-2.351 3.17-.769.216-1.001.585-.967.702.033.117.423.311 1.192.095.53-.149 1.084.16 1.233.691.148.532-.161 1.084-.693 1.234-1.971.555-3.333-.323-3.659-1.479-.324-1.154.379-2.613 2.353-3.169.77-.217 1.001-.584.967-.702-.032-.117-.422-.312-1.19-.096-1.974.556-3.334-.322-3.659-1.479-.325-1.154.378-2.613 2.351-3.17.768-.215.999-.585.967-.701-.034-.118-.423-.312-1.192-.096-.532.15-1.083-.16-1.233-.691-.149-.53.161-1.082.693-1.232z"/><path fill="#AA8DD8" d="M23.001 20.16c-.294 0-.584-.129-.782-.375-.345-.432-.274-1.061.156-1.406.218-.175 5.418-4.259 12.767-3.208.547.078.927.584.849 1.131-.078.546-.58.93-1.132.848-6.493-.922-11.187 2.754-11.233 2.791-.186.148-.406.219-.625.219z"/><path fill="#77B255" d="M5.754 16c-.095 0-.192-.014-.288-.042-.529-.159-.829-.716-.67-1.245 1.133-3.773 2.16-9.794.898-11.364-.141-.178-.354-.353-.842-.316-.938.072-.849 2.051-.848 2.071.042.551-.372 1.031-.922 1.072-.559.034-1.031-.372-1.072-.923-.103-1.379.326-4.035 2.692-4.214 1.056-.08 1.933.287 2.552 1.057 2.371 2.951-.036 11.506-.542 13.192-.13.433-.528.712-.958.712z"/><circle fill="#5C913B" cx="25.5" cy="9.5" r="1.5"/><circle fill="#9266CC" cx="2" cy="18" r="2"/><circle fill="#5C913B" cx="32.5" cy="19.5" r="1.5"/><circle fill="#5C913B" cx="23.5" cy="31.5" r="1.5"/><circle fill="#FFCC4D" cx="28" cy="4" r="2"/><circle fill="#FFCC4D" cx="32.5" cy="8.5" r="1.5"/><circle fill="#FFCC4D" cx="29.5" cy="12.5" r="1.5"/><circle fill="#FFCC4D" cx="7.5" cy="23.5" r="1.5"/></svg>
|
After Width: | Height: | Size: 3.1 KiB |
|
@ -4,8 +4,32 @@ body {
|
|||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
center {
|
||||
margin: auto;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
|
||||
p {
|
||||
border-right: 1px dotted grey;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
button p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
@ -19,11 +43,38 @@ h1 {
|
|||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #CCC;
|
||||
button.button {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
button.main_file_upload {
|
||||
border: 1px solid grey;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
width: 250px;
|
||||
height: 75px;
|
||||
cursor: pointer;
|
||||
background-color: #84E5FF;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 1.9em;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.button.selected {
|
||||
background-color: #84FFAE;
|
||||
border: 2px dashed grey;
|
||||
}
|
||||
|
||||
#durationBox {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: fit-content;
|
||||
|
@ -36,3 +87,43 @@ h1 {
|
|||
height: 40px;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
.progress_box {
|
||||
display: flex;
|
||||
width: 300px;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
height: 70px;
|
||||
|
||||
progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress_value {
|
||||
width: 90px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#uploadedFilesDisplay {
|
||||
text-align: left;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
#uploadedFilesDisplay div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#uploadedFilesDisplay > div > span {
|
||||
flex-grow: 2;
|
||||
border-top: 2px dotted grey;
|
||||
height: 0px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#uploadedFilesDisplay button {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ let CAPABILITIES;
|
|||
|
||||
async function formSubmit(form) {
|
||||
if (uploadInProgress) {
|
||||
return;
|
||||
return; // TODO: REMOVE THIS ONCE MULTIPLE CAN WORK!
|
||||
}
|
||||
|
||||
// Get file size and don't upload if it's too large
|
||||
|
@ -57,6 +57,7 @@ function networkErrorHandler(_err) {
|
|||
|
||||
function uploadComplete(response) {
|
||||
let target = response.target;
|
||||
progressBar.value = 0;
|
||||
|
||||
if (target.status === 200) {
|
||||
const response = JSON.parse(target.responseText);
|
||||
|
@ -93,10 +94,9 @@ function uploadProgress(progress) {
|
|||
}
|
||||
}
|
||||
|
||||
// This is the entrypoint for everything basically
|
||||
document.addEventListener("DOMContentLoaded", function(_event){
|
||||
document.getElementById("uploadForm").addEventListener("submit", formSubmit);
|
||||
progressBar = document.getElementById("uploadProgress");
|
||||
progressValue = document.getElementById("uploadProgressValue");
|
||||
statusNotifier = document.getElementById("uploadStatus");
|
||||
uploadedFilesDisplay = document.getElementById("uploadedFilesDisplay");
|
||||
durationBox = document.getElementById("durationBox");
|
||||
|
@ -106,27 +106,32 @@ document.addEventListener("DOMContentLoaded", function(_event){
|
|||
|
||||
function toPrettyTime(seconds) {
|
||||
var days = Math.floor(seconds / 86400);
|
||||
var hours = Math.floor((seconds - (days * 86400)) / 3600);
|
||||
var mins = Math.floor((seconds - (hours * 3600) - (days * 86400)) / 60);
|
||||
var secs = seconds - (hours * 3600) - (mins * 60) - (days * 86400);
|
||||
var hour = Math.floor((seconds - (days * 86400)) / 3600);
|
||||
var mins = Math.floor((seconds - (hour * 3600) - (days * 86400)) / 60);
|
||||
var secs = seconds - (hour * 3600) - (mins * 60) - (days * 86400);
|
||||
|
||||
if(days == 0) {days = "";} else if(days == 1) {days += "<br>day"} else {days += "<br>days"}
|
||||
if(hours == 0) {hours = "";} else if(hours == 1) {hours += "<br>hour"} else {hours += "<br>hours"}
|
||||
if(hour == 0) {hour = "";} else if(hour == 1) {hour += "<br>hour"} else {hour += "<br>hours"}
|
||||
if(mins == 0) {mins = "";} else if(mins == 1) {mins += "<br>minute"} else {mins += "<br>minutes"}
|
||||
if(secs == 0) {secs = "";} else if(secs == 1) {secs += "<br>second"} else {secs += "<br>seconds"}
|
||||
|
||||
return (days + " " + hours + " " + mins + " " + secs).trim();
|
||||
return (days + " " + hour + " " + mins + " " + secs).trim();
|
||||
}
|
||||
|
||||
async function getServerCapabilities() {
|
||||
CAPABILITIES = await fetch("info").then((response) => response.json());
|
||||
|
||||
let file_duration = document.getElementById("fileDuration");
|
||||
file_duration.value = CAPABILITIES.default_duration + "s";
|
||||
|
||||
for (duration in CAPABILITIES.allowed_durations) {
|
||||
const durationOption = durationBox.appendChild(document.createElement("p"));
|
||||
durationOption.innerHTML = toPrettyTime(CAPABILITIES.allowed_durations[duration]);
|
||||
durationOption.classList.add("button");
|
||||
const durationButtons = durationBox.getElementsByTagName("button");
|
||||
for (const b of durationButtons) {
|
||||
b.addEventListener("click", function (_e) {
|
||||
if (this.classList.contains("selected")) {
|
||||
return
|
||||
}
|
||||
let selected = this.parentNode.getElementsByClassName("selected");
|
||||
selected[0].classList.remove("selected");
|
||||
|
||||
file_duration.value = this.dataset.durationSeconds + "s";
|
||||
this.classList.add("selected");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
50
src/strings.rs
Normal file
50
src/strings.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use std::error::Error;
|
||||
|
||||
use chrono::TimeDelta;
|
||||
|
||||
pub fn parse_time_string(string: &str) -> Result<TimeDelta, Box<dyn Error>> {
|
||||
if string.len() > 7 {
|
||||
return Err("Not valid time string".into())
|
||||
}
|
||||
|
||||
let unit = string.chars().last();
|
||||
let multiplier = if let Some(u) = unit {
|
||||
if !u.is_ascii_alphabetic() {
|
||||
return Err("Not valid time string".into())
|
||||
}
|
||||
|
||||
match u {
|
||||
'D' | 'd' => TimeDelta::days(1),
|
||||
'H' | 'h' => TimeDelta::hours(1),
|
||||
'M' | 'm' => TimeDelta::minutes(1),
|
||||
'S' | 's' => TimeDelta::seconds(1),
|
||||
_ => return Err("Not valid time string".into()),
|
||||
}
|
||||
} else {
|
||||
return Err("Not valid time string".into())
|
||||
};
|
||||
|
||||
let time = if let Ok(n) = string[..string.len() - 1].parse::<i32>() {
|
||||
n
|
||||
} else {
|
||||
return Err("Not valid time string".into())
|
||||
};
|
||||
|
||||
let final_time = multiplier * time;
|
||||
|
||||
Ok(final_time)
|
||||
}
|
||||
|
||||
pub fn to_pretty_time(seconds: u32) -> String {
|
||||
let days = (seconds as f32 / 86400.0).floor();
|
||||
let hour = ((seconds as f32 - (days as f32 * 86400.0)) / 3600.0).floor();
|
||||
let mins = ((seconds as f32 - (hour * 3600.0) - (days * 86400.0)) / 60.0).floor();
|
||||
let secs = seconds as f32 - (hour as f32 * 3600.0) - (mins as f32 * 60.0) - (days as f32 * 86400.0);
|
||||
|
||||
let days = if days == 0.0 {"".to_string()} else if days == 1.0 {days.to_string() + "<br>day"} else {days.to_string() + "<br>days"};
|
||||
let hour = if hour == 0.0 {"".to_string()} else if hour == 1.0 {hour.to_string() + "<br>hour"} else {hour.to_string() + "<br>hours"};
|
||||
let mins = if mins == 0.0 {"".to_string()} else if mins == 1.0 {mins.to_string() + "<br>minute"} else {mins.to_string() + "<br>minutes"};
|
||||
let secs = if secs == 0.0 {"".to_string()} else if secs == 1.0 {secs.to_string() + "<br>second"} else {secs.to_string() + "<br>seconds"};
|
||||
|
||||
(days + " " + &hour + " " + &mins + " " + &secs).trim().to_string()
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
use std::error::Error;
|
||||
|
||||
use chrono::TimeDelta;
|
||||
|
||||
pub fn parse_time_string(string: &str) -> Result<TimeDelta, Box<dyn Error>> {
|
||||
if string.len() > 7 {
|
||||
return Err("Not valid time string".into())
|
||||
}
|
||||
|
||||
let unit = string.chars().last();
|
||||
let multiplier = if let Some(u) = unit {
|
||||
if !u.is_ascii_alphabetic() {
|
||||
return Err("Not valid time string".into())
|
||||
}
|
||||
|
||||
match u {
|
||||
'D' | 'd' => TimeDelta::days(1),
|
||||
'H' | 'h' => TimeDelta::hours(1),
|
||||
'M' | 'm' => TimeDelta::minutes(1),
|
||||
'S' | 's' => TimeDelta::seconds(1),
|
||||
_ => return Err("Not valid time string".into()),
|
||||
}
|
||||
} else {
|
||||
return Err("Not valid time string".into())
|
||||
};
|
||||
|
||||
let time = if let Ok(n) = string[..string.len() - 1].parse::<i32>() {
|
||||
n
|
||||
} else {
|
||||
return Err("Not valid time string".into())
|
||||
};
|
||||
|
||||
let final_time = multiplier * time;
|
||||
|
||||
Ok(final_time)
|
||||
}
|
Loading…
Reference in a new issue