From 651b62a191e8447ff57cad759f68f4442e31f977 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 4 Jan 2026 00:38:55 -0500 Subject: [PATCH 1/4] Lorem Ipsum generator --- Cargo.lock | 11 +++ Cargo.toml | 1 + src/pages/generator/lorem_ipsum.rs | 114 +++++++++++++++++++++++++++++ src/pages/generator/mod.rs | 5 ++ 4 files changed, 131 insertions(+) create mode 100644 src/pages/generator/lorem_ipsum.rs diff --git a/Cargo.lock b/Cargo.lock index 9fd8c42..34baa58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -796,6 +796,7 @@ dependencies = [ "dioxus-free-icons", "dioxus-sdk", "getrandom 0.3.4", + "lipsum", "log", "manganis", "md-5", @@ -2792,6 +2793,16 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "lipsum" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064" +dependencies = [ + "rand 0.8.5", + "rand_chacha 0.3.1", +] + [[package]] name = "litemap" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index f2d21b6..c0a2a22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ strum_macros = "0.27" time = "0.3" time-tz = { version = "2.0", features = ["db", "system"] } uuid = { version = "1.19", features = ["v4", "v7", "rng-getrandom"] } +lipsum = "0.9" wasm-bindgen = { version = "0.2.100", features = ["enable-interning"], optional = true } wasm-logger = { version = "0.2.0", optional = true } diff --git a/src/pages/generator/lorem_ipsum.rs b/src/pages/generator/lorem_ipsum.rs new file mode 100644 index 0000000..6b622ac --- /dev/null +++ b/src/pages/generator/lorem_ipsum.rs @@ -0,0 +1,114 @@ +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_free_icons::icons::fa_solid_icons::FaAlignLeft; +use strum_macros::{Display, EnumIter, EnumString, IntoStaticStr}; + +use crate::{ + components::inputs::{NumberInput, SelectForm, SelectFormEnum, SwitchInput, TextAreaForm}, + pages::{WidgetEntry, WidgetIcon}, +}; + +pub const WIDGET_ENTRY: WidgetEntry = WidgetEntry { + title: "Lorem Ipsum Generator", + short_title: "Lorem Ipsum", + description: "Generate placeholder text", + icon: move || ICON.icon(), +}; + +const ICON: WidgetIcon = WidgetIcon { icon: FaAlignLeft }; + +#[derive(Copy, Clone, Default, Debug, Display, EnumIter, EnumString, Hash, IntoStaticStr, PartialEq)] +enum LoremMode { + #[default] + Paragraphs, + Sentences, + Words, +} + +impl SelectFormEnum for LoremMode {} + +impl From for String { + fn from(mode: LoremMode) -> Self { + mode.to_string() + } +} + +#[component] +pub fn LoremIpsum() -> Element { + let mut mode = use_signal(|| LoremMode::Paragraphs); + let mut count = use_signal(|| 3usize); + let mut start_with_lorem = use_signal(|| true); + + let generated_text = { + let mode_val = *mode.read(); + let count_val = *count.read(); + let start_lorem = *start_with_lorem.read(); + + match mode_val { + LoremMode::Paragraphs => { + let paragraphs: Vec = (0..count_val) + .map(|i| { + if i == 0 && start_lorem { + lipsum::lipsum(50) + } else { + lipsum::lipsum_words(50) + } + }) + .collect(); + paragraphs.join("\n\n") + } + LoremMode::Sentences => { + let word_count = count_val * 10; + let text = if start_lorem { + lipsum::lipsum(word_count) + } else { + lipsum::lipsum_words(word_count) + }; + // Split into sentences and take the requested count + let sentences: Vec<&str> = text.split(". ").take(count_val).collect(); + let mut result = sentences.join(". "); + if !result.ends_with('.') { + result.push('.'); + } + result + } + LoremMode::Words => { + if start_lorem { + lipsum::lipsum(count_val) + } else { + lipsum::lipsum_words(count_val) + } + } + } + }; + + rsx! { + div { class: "lorem-ipsum-generator", + div { class: "params", + SelectForm:: { + label: "Mode", + value: *mode.read(), + oninput: move |value| mode.set(value), + } + NumberInput:: { + label: "Count", + value: *count.read(), + onchange: move |value: usize| count.set(value.clamp(1, 50)), + } + div { class: "switches", + SwitchInput { + label: "Start with \"Lorem ipsum...\"", + checked: *start_with_lorem.read(), + oninput: move |value| start_with_lorem.set(value), + } + } + } + + TextAreaForm { + label: "Generated Text", + value: "{generated_text}", + readonly: true, + } + } + } +} diff --git a/src/pages/generator/mod.rs b/src/pages/generator/mod.rs index aab2a27..a417bc4 100644 --- a/src/pages/generator/mod.rs +++ b/src/pages/generator/mod.rs @@ -4,6 +4,7 @@ use dioxus_free_icons::Icon; use strum_macros::EnumIter; pub mod hash_generator; +pub mod lorem_ipsum; pub mod qr_code_generator; pub mod uuid_generator; @@ -19,6 +20,7 @@ pub static CATEGORY_ENTRY: CategoryEntry = CategoryEntry { }, }; use hash_generator::HashGenerator; +use lorem_ipsum::LoremIpsum; use qr_code_generator::QrCodeGenerator; use uuid_generator::UuidGenerator; @@ -28,6 +30,8 @@ pub enum GeneratorRoute { Index {}, #[route("/hash")] HashGenerator {}, + #[route("/lorem-ipsum")] + LoremIpsum {}, #[route("/qr-code")] QrCodeGenerator {}, #[route("/uuid")] @@ -57,6 +61,7 @@ impl WidgetRoute for GeneratorRoute { fn get_widget_entry(&self) -> Option<&'static WidgetEntry> { match self { Self::HashGenerator { .. } => Some(&hash_generator::WIDGET_ENTRY), + Self::LoremIpsum { .. } => Some(&lorem_ipsum::WIDGET_ENTRY), Self::QrCodeGenerator { .. } => Some(&qr_code_generator::WIDGET_ENTRY), Self::UuidGenerator { .. } => Some(&uuid_generator::WIDGET_ENTRY), _ => None, From 0bfa6a765b69ab52f04f392e4be8f5236ae8cdcd Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 4 Jan 2026 00:39:19 -0500 Subject: [PATCH 2/4] Password Generator --- src/pages/generator/mod.rs | 5 + src/pages/generator/password_generator.rs | 176 ++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 src/pages/generator/password_generator.rs diff --git a/src/pages/generator/mod.rs b/src/pages/generator/mod.rs index aab2a27..200e276 100644 --- a/src/pages/generator/mod.rs +++ b/src/pages/generator/mod.rs @@ -4,6 +4,7 @@ use dioxus_free_icons::Icon; use strum_macros::EnumIter; pub mod hash_generator; +pub mod password_generator; pub mod qr_code_generator; pub mod uuid_generator; @@ -19,6 +20,7 @@ pub static CATEGORY_ENTRY: CategoryEntry = CategoryEntry { }, }; use hash_generator::HashGenerator; +use password_generator::PasswordGenerator; use qr_code_generator::QrCodeGenerator; use uuid_generator::UuidGenerator; @@ -28,6 +30,8 @@ pub enum GeneratorRoute { Index {}, #[route("/hash")] HashGenerator {}, + #[route("/password")] + PasswordGenerator {}, #[route("/qr-code")] QrCodeGenerator {}, #[route("/uuid")] @@ -57,6 +61,7 @@ impl WidgetRoute for GeneratorRoute { fn get_widget_entry(&self) -> Option<&'static WidgetEntry> { match self { Self::HashGenerator { .. } => Some(&hash_generator::WIDGET_ENTRY), + Self::PasswordGenerator { .. } => Some(&password_generator::WIDGET_ENTRY), Self::QrCodeGenerator { .. } => Some(&qr_code_generator::WIDGET_ENTRY), Self::UuidGenerator { .. } => Some(&uuid_generator::WIDGET_ENTRY), _ => None, diff --git a/src/pages/generator/password_generator.rs b/src/pages/generator/password_generator.rs new file mode 100644 index 0000000..db52a96 --- /dev/null +++ b/src/pages/generator/password_generator.rs @@ -0,0 +1,176 @@ +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_free_icons::icons::fa_solid_icons::FaKey; +use rand::Rng; + +use crate::{ + components::inputs::{NumberInput, SwitchInput, TextAreaForm}, + pages::{WidgetEntry, WidgetIcon}, +}; + +pub const WIDGET_ENTRY: WidgetEntry = WidgetEntry { + title: "Password Generator", + short_title: "Password", + description: "Generate secure, customizable passwords", + icon: move || ICON.icon(), +}; + +const ICON: WidgetIcon = WidgetIcon { icon: FaKey }; + +const UPPERCASE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const LOWERCASE: &str = "abcdefghijklmnopqrstuvwxyz"; +const NUMBERS: &str = "0123456789"; +const SYMBOLS: &str = "!@#$%^&*()_+-=[]{}|;:,.<>?"; +const AMBIGUOUS: &str = "0O1lI"; + +#[component] +pub fn PasswordGenerator() -> Element { + let mut length = use_signal(|| 16usize); + let mut use_uppercase = use_signal(|| true); + let mut use_lowercase = use_signal(|| true); + let mut use_numbers = use_signal(|| true); + let mut use_symbols = use_signal(|| true); + let mut exclude_ambiguous = use_signal(|| false); + let mut quantity = use_signal(|| 1usize); + let mut passwords = use_signal(Vec::::new); + + let generate_passwords = move |_| { + let mut charset = String::new(); + + if *use_uppercase.read() { + charset.push_str(UPPERCASE); + } + if *use_lowercase.read() { + charset.push_str(LOWERCASE); + } + if *use_numbers.read() { + charset.push_str(NUMBERS); + } + if *use_symbols.read() { + charset.push_str(SYMBOLS); + } + + if *exclude_ambiguous.read() { + charset = charset.chars().filter(|c| !AMBIGUOUS.contains(*c)).collect(); + } + + if charset.is_empty() { + return; + } + + let charset_chars: Vec = charset.chars().collect(); + let mut rng = rand::rng(); + let mut new_passwords = Vec::new(); + + for _ in 0..*quantity.read() { + let password: String = (0..*length.read()) + .map(|_| charset_chars[rng.random_range(0..charset_chars.len())]) + .collect(); + new_passwords.push(password); + } + + passwords.write().append(&mut new_passwords); + }; + + // Calculate entropy + let charset_size = { + let mut size = 0usize; + if *use_uppercase.read() { size += 26; } + if *use_lowercase.read() { size += 26; } + if *use_numbers.read() { size += 10; } + if *use_symbols.read() { size += SYMBOLS.len(); } + if *exclude_ambiguous.read() && size > 0 { + size = size.saturating_sub(5); // Approximate ambiguous chars removed + } + size + }; + let entropy = if charset_size > 0 { + (*length.read() as f64) * (charset_size as f64).log2() + } else { + 0.0 + }; + let entropy_label = if entropy >= 128.0 { + "Very Strong" + } else if entropy >= 80.0 { + "Strong" + } else if entropy >= 60.0 { + "Moderate" + } else if entropy >= 40.0 { + "Weak" + } else { + "Very Weak" + }; + + let passwords_str = passwords.with(|p| p.join("\n")); + + rsx! { + div { class: "password-generator", + div { class: "params", + NumberInput:: { + label: "Password Length", + value: *length.read(), + onchange: move |value: usize| { + length.set(value.clamp(4, 128)); + }, + } + NumberInput:: { + label: "Number of Passwords", + value: *quantity.read(), + onchange: move |value: usize| { + quantity.set(value.clamp(1, 100)); + }, + } + div { class: "switches", + SwitchInput { + label: "Uppercase (A-Z)", + checked: *use_uppercase.read(), + oninput: move |value| use_uppercase.set(value), + } + SwitchInput { + label: "Lowercase (a-z)", + checked: *use_lowercase.read(), + oninput: move |value| use_lowercase.set(value), + } + SwitchInput { + label: "Numbers (0-9)", + checked: *use_numbers.read(), + oninput: move |value| use_numbers.set(value), + } + SwitchInput { + label: "Symbols (!@#...)", + checked: *use_symbols.read(), + oninput: move |value| use_symbols.set(value), + } + SwitchInput { + label: "Exclude Ambiguous (0O1lI)", + checked: *exclude_ambiguous.read(), + oninput: move |value| exclude_ambiguous.set(value), + } + } + } + + div { class: "entropy-display", + "Entropy: {entropy:.0} bits ({entropy_label})" + } + + div { class: "buttons", + button { + class: "btn btn-info me-3", + onclick: generate_passwords, + "Generate" + } + button { + class: "btn btn-error", + onclick: move |_| passwords.write().clear(), + "Clear" + } + } + + TextAreaForm { + label: "Generated Passwords", + value: "{passwords_str}", + readonly: true, + } + } + } +} From df3797fe25e04559b01a9f57fbe13719e6831e5f Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 4 Jan 2026 01:13:16 -0500 Subject: [PATCH 3/4] use rng --- Cargo.lock | 2 ++ Cargo.toml | 4 ++- src/main.css | 1 + src/pages/generator/lorem_ipsum.css | 31 ++++++++++++++++++++++ src/pages/generator/lorem_ipsum.rs | 40 +++++++++++++++++++++-------- 5 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 src/pages/generator/lorem_ipsum.css diff --git a/Cargo.lock b/Cargo.lock index 34baa58..5c5f1a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,6 +795,7 @@ dependencies = [ "dioxus", "dioxus-free-icons", "dioxus-sdk", + "getrandom 0.2.15", "getrandom 0.3.4", "lipsum", "log", @@ -802,6 +803,7 @@ dependencies = [ "md-5", "num-traits", "qrcode-generator", + "rand 0.8.5", "serde", "sha1", "sha2", diff --git a/Cargo.toml b/Cargo.toml index c0a2a22..c0ceb90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ digest = "0.10" dioxus = { version = "0.7.2", features = ["router"] } dioxus-free-icons = { version = "0.10.0", features = ["bootstrap", "font-awesome-solid"] } dioxus-sdk = { version = "0.7", features = ["storage"] } +getrandom_02 = { package = "getrandom", version = "0.2", optional = true } getrandom = "0.3" log = { version = "0.4", features = ["std"] } manganis = "0.7.2" @@ -28,12 +29,13 @@ time = "0.3" time-tz = { version = "2.0", features = ["db", "system"] } uuid = { version = "1.19", features = ["v4", "v7", "rng-getrandom"] } lipsum = "0.9" +rand = { version = "0.8", features = ["getrandom"] } wasm-bindgen = { version = "0.2.100", features = ["enable-interning"], optional = true } wasm-logger = { version = "0.2.0", optional = true } [features] desktop = ["dioxus/desktop"] -web = ["dioxus/web", "getrandom/wasm_js", "time/wasm-bindgen", "uuid/js", "dep:wasm-bindgen", "dep:wasm-logger"] +web = ["dioxus/web", "getrandom/wasm_js", "getrandom_02/js", "dep:getrandom_02", "time/wasm-bindgen", "uuid/js", "dep:wasm-bindgen", "dep:wasm-logger"] default = ["desktop"] diff --git a/src/main.css b/src/main.css index a526f7b..a28c15e 100644 --- a/src/main.css +++ b/src/main.css @@ -21,4 +21,5 @@ @import "./pages/generator/hash_generator.css"; @import "./pages/generator/qr_code_generator.css"; @import "./pages/generator/uuid_generator.css"; +@import "./pages/generator/lorem_ipsum.css"; @import "./pages/media/color_picker.css"; diff --git a/src/pages/generator/lorem_ipsum.css b/src/pages/generator/lorem_ipsum.css new file mode 100644 index 0000000..743ebef --- /dev/null +++ b/src/pages/generator/lorem_ipsum.css @@ -0,0 +1,31 @@ +/* Lorem Ipsum Generator */ +@layer components { + .lorem-ipsum-generator { + @apply flex flex-col gap-y-3 h-full; + } + + .lorem-ipsum-generator .textarea-form { + flex: 1 1 auto; + } + + .lorem-ipsum-generator .params { + @apply flex flex-row gap-x-3 gap-y-3 flex-wrap; + } + + .lorem-ipsum-generator .params > div:not(.switches):not(.buttons) { + @apply flex grow; + min-width: 225px; + } + + .lorem-ipsum-generator .params .buttons { + @apply flex flex-row gap-x-3 items-center; + } + + .lorem-ipsum-generator .params .switches { + @apply flex flex-col gap-y-3 justify-center; + } + + .lorem-ipsum-generator .params .switches .switch { + @apply gap-x-1; + } +} diff --git a/src/pages/generator/lorem_ipsum.rs b/src/pages/generator/lorem_ipsum.rs index 6b622ac..d14ec4d 100644 --- a/src/pages/generator/lorem_ipsum.rs +++ b/src/pages/generator/lorem_ipsum.rs @@ -1,6 +1,7 @@ #![allow(non_snake_case)] use dioxus::prelude::*; use dioxus_free_icons::icons::fa_solid_icons::FaAlignLeft; +use rand::thread_rng; use strum_macros::{Display, EnumIter, EnumString, IntoStaticStr}; use crate::{ @@ -17,7 +18,9 @@ pub const WIDGET_ENTRY: WidgetEntry = WidgetEntry { const ICON: WidgetIcon = WidgetIcon { icon: FaAlignLeft }; -#[derive(Copy, Clone, Default, Debug, Display, EnumIter, EnumString, Hash, IntoStaticStr, PartialEq)] +#[derive( + Copy, Clone, Default, Debug, Display, EnumIter, EnumString, Hash, IntoStaticStr, PartialEq, +)] enum LoremMode { #[default] Paragraphs, @@ -38,31 +41,33 @@ pub fn LoremIpsum() -> Element { let mut mode = use_signal(|| LoremMode::Paragraphs); let mut count = use_signal(|| 3usize); let mut start_with_lorem = use_signal(|| true); + let mut generated_text = use_signal(String::new); - let generated_text = { + let generate = move |_| { let mode_val = *mode.read(); let count_val = *count.read(); let start_lorem = *start_with_lorem.read(); + let mut rng = thread_rng(); - match mode_val { + let text = match mode_val { LoremMode::Paragraphs => { let paragraphs: Vec = (0..count_val) .map(|i| { if i == 0 && start_lorem { - lipsum::lipsum(50) + lipsum::lipsum_with_rng(&mut rng, 50) } else { - lipsum::lipsum_words(50) + lipsum::lipsum_words_with_rng(&mut rng, 50) } }) .collect(); paragraphs.join("\n\n") } LoremMode::Sentences => { - let word_count = count_val * 10; + let word_count = count_val * 12; let text = if start_lorem { - lipsum::lipsum(word_count) + lipsum::lipsum_with_rng(&mut rng, word_count) } else { - lipsum::lipsum_words(word_count) + lipsum::lipsum_words_with_rng(&mut rng, word_count) }; // Split into sentences and take the requested count let sentences: Vec<&str> = text.split(". ").take(count_val).collect(); @@ -74,12 +79,13 @@ pub fn LoremIpsum() -> Element { } LoremMode::Words => { if start_lorem { - lipsum::lipsum(count_val) + lipsum::lipsum_with_rng(&mut rng, count_val) } else { - lipsum::lipsum_words(count_val) + lipsum::lipsum_words_with_rng(&mut rng, count_val) } } - } + }; + generated_text.set(text); }; rsx! { @@ -95,6 +101,18 @@ pub fn LoremIpsum() -> Element { value: *count.read(), onchange: move |value: usize| count.set(value.clamp(1, 50)), } + div { class: "buttons", + button { + class: "btn btn-info", + onclick: generate, + "Generate" + } + button { + class: "btn btn-error", + onclick: move |_| generated_text.set(String::new()), + "Clear" + } + } div { class: "switches", SwitchInput { label: "Start with \"Lorem ipsum...\"", From 7cbdfa16b111173519cebe9ea0ad19c6be34dd44 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Sun, 4 Jan 2026 01:34:54 -0500 Subject: [PATCH 4/4] fix styling --- src/main.css | 1 + src/pages/generator/password_generator.css | 31 ++++++++++++++++++++++ src/pages/generator/password_generator.rs | 17 ++++++------ 3 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 src/pages/generator/password_generator.css diff --git a/src/main.css b/src/main.css index 5d24b8f..45b0987 100644 --- a/src/main.css +++ b/src/main.css @@ -23,4 +23,5 @@ @import "./pages/generator/qr_code_generator.css"; @import "./pages/generator/uuid_generator.css"; @import "./pages/generator/lorem_ipsum.css"; +@import "./pages/generator/password_generator.css"; @import "./pages/media/color_picker.css"; diff --git a/src/pages/generator/password_generator.css b/src/pages/generator/password_generator.css new file mode 100644 index 0000000..f75d5fb --- /dev/null +++ b/src/pages/generator/password_generator.css @@ -0,0 +1,31 @@ +/* Password Generator */ +@layer components { + .password-generator { + @apply flex flex-col gap-y-3 h-full; + } + + .password-generator .textarea-form { + flex: 1 1 auto; + } + + .password-generator .params { + @apply flex flex-row gap-x-3 gap-y-3 flex-wrap; + } + + .password-generator .params > div:not(.switches):not(.buttons) { + @apply flex grow; + min-width: 225px; + } + + .password-generator .params .buttons { + @apply flex flex-row gap-x-3 items-center; + } + + .password-generator .params .switches { + @apply flex flex-row gap-x-3 gap-y-3 flex-wrap items-center; + } + + .password-generator .params .switches .switch { + @apply gap-x-1; + } +} diff --git a/src/pages/generator/password_generator.rs b/src/pages/generator/password_generator.rs index a8ac0b8..58c97e6 100644 --- a/src/pages/generator/password_generator.rs +++ b/src/pages/generator/password_generator.rs @@ -131,6 +131,14 @@ pub fn PasswordGenerator() -> Element { quantity.set(value.clamp(1, 100)); }, } + div { class: "buttons", + button { class: "btn btn-info", onclick: generate_passwords, "Generate" } + button { + class: "btn btn-error", + onclick: move |_| passwords.write().clear(), + "Clear" + } + } div { class: "switches", SwitchInput { label: "Uppercase (A-Z)", @@ -162,15 +170,6 @@ pub fn PasswordGenerator() -> Element { div { class: "entropy-display", "Entropy: {entropy:.0} bits ({entropy_label})" } - div { class: "buttons", - button { class: "btn btn-info me-3", onclick: generate_passwords, "Generate" } - button { - class: "btn btn-error", - onclick: move |_| passwords.write().clear(), - "Clear" - } - } - TextAreaForm { label: "Generated Passwords", value: "{passwords_str}",