Compare commits

..

2 Commits

2 changed files with 170 additions and 30 deletions

View File

@ -1,5 +1,5 @@
use cipher_factory::prelude::*; use cipher_factory::prelude::*;
use leptos::prelude::*; use leptos::{prelude::*, tachys::dom::event_target_value};
use std::{str::FromStr, time::Duration}; use std::{str::FromStr, time::Duration};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use web_sys::WheelEvent; use web_sys::WheelEvent;
@ -22,14 +22,20 @@ pub fn CipherForm(algorithm: Algorithm) -> impl IntoView {
set_output(String::new()); set_output(String::new());
let key = key_input.get(); let key = key_input.get();
let text = text_input.get(); let raw_text = text_input.get();
if key.is_empty() || text.is_empty() { if key.is_empty() || raw_text.is_empty() {
set_error_msg("Please enter both key and input text.".to_string()); set_error_msg("Please enter both key and input text.".to_string());
return; return;
} }
let context = CipherContext::new(algorithm, mode.get(), key, text, output_fmt.get()); let final_text = if mode.get() == OperationMode::Decrypt {
format!("0x{raw_text}")
} else {
raw_text
};
let context = CipherContext::new(algorithm, mode.get(), key, final_text, output_fmt.get());
match context.process() { match context.process() {
Ok(out) => set_output(out), Ok(out) => set_output(out),
Err(e) => set_error_msg(e.to_string()), Err(e) => set_error_msg(e.to_string()),
@ -62,7 +68,7 @@ pub fn CipherForm(algorithm: Algorithm) -> impl IntoView {
update_output=update_output update_output=update_output
/> />
<KeyInput set_key_input=set_key_input /> <KeyInput set_key_input=set_key_input />
<TextInput mode=mode set_text_input=set_text_input /> <TextInput mode=mode text_input=text_input set_text_input=set_text_input />
<button class="btn-primary" on:click=move |_| handle_submit()> <button class="btn-primary" on:click=move |_| handle_submit()>
{move || format!("{} using {algorithm}", mode.get())} {move || format!("{} using {algorithm}", mode.get())}
@ -173,15 +179,24 @@ fn ConfigurationSection(
} }
} }
fn clean_hex_input(input: String) -> String {
input
.chars()
.filter(|ch| ch.is_ascii_hexdigit())
.collect::<String>()
}
#[component] #[component]
fn KeyInput(set_key_input: WriteSignal<String>) -> impl IntoView { fn KeyInput(set_key_input: WriteSignal<String>) -> impl IntoView {
view! { view! {
<div class="form-group"> <div class="form-group">
<div class="lable-header">
<label>"Secret Key"</label> <label>"Secret Key"</label>
<span class="input-hint">"Prefix: 0x (Hex), 0b (Bin), or nothing (Text)"</span>
</div>
<input <input
type="text" type="text"
prop:key_input placeholder="Enter key (e.g., 0x1A2B...)"
placeholder="Enter key..."
on:input=move |ev| set_key_input(event_target_value(&ev)) on:input=move |ev| set_key_input(event_target_value(&ev))
/> />
</div> </div>
@ -191,26 +206,64 @@ fn KeyInput(set_key_input: WriteSignal<String>) -> impl IntoView {
#[component] #[component]
fn TextInput( fn TextInput(
mode: ReadSignal<OperationMode>, mode: ReadSignal<OperationMode>,
text_input: ReadSignal<String>,
set_text_input: WriteSignal<String>, set_text_input: WriteSignal<String>,
) -> impl IntoView { ) -> impl IntoView {
let handle_hex_input = move |ev| {
let val = event_target_value(&ev);
let cleaned = clean_hex_input(val);
set_text_input(cleaned);
};
let handle_text_input = move |ev| {
set_text_input(event_target_value(&ev));
};
view! { view! {
<div class="form-group"> <div class="form-group">
<label>
{move || { {move || {
match mode.get() { match mode.get() {
OperationMode::Encrypt => "Plaintext Input", OperationMode::Encrypt => {
OperationMode::Decrypt => "Ciphertext (Hex) Input", view! {
} <div class="lable-header">
}} <label>"Plaintext Input"</label>
</label> <span class="input-hint">
"Prefix: 0x (Hex), 0b (Bin), or nothing (Text)"
</span>
</div>
<div class="input-wrapper standard-input">
<input <input
type="text" type="text"
prop:text_input
placeholder="Enter text..." placeholder="Enter text..."
on:input=move |ev| set_text_input(event_target_value(&ev)) on:input=handle_text_input
spellcheck="false"
/> />
</div> </div>
} }
.into_any()
}
OperationMode::Decrypt => {
view! {
<div class="lable-header">
<label>"Ciphertext Input"</label>
</div>
<div class="input-wrapper hex-input">
<span class="prefix">"0x"</span>
<input
type="text"
prop:value=move || text_input.get()
placeholder="001122"
on:input=handle_hex_input
spellcheck="false"
/>
</div>
}
.into_any()
}
}
}}
</div>
}
} }
#[component] #[component]

View File

@ -312,6 +312,35 @@ main {
} }
} }
@mixin input-styles {
width: 100%;
background: var(--bg-input);
color: var(--text-main);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 1rem;
font-family: "Consolas", "Monaco", monospace;
box-sizing: border-box;
transition:
background $trans-time,
border-color $trans-time,
box-shadow $trans-time,
color $trans-time;
&:focus,
&:focus-within {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--focus-ring);
}
&::placeholder {
color: var(--text-muted);
opacity: 0.5;
}
}
.form-group { .form-group {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
@ -327,15 +356,8 @@ main {
input[type="text"], input[type="text"],
textarea { textarea {
width: 100%; @include input-styles;
padding: 12px; padding: 12px;
background: var(--bg-input);
color: var(--text-main);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 1rem;
box-sizing: border-box;
font-family: monospace;
} }
&:focus { &:focus {
@ -549,7 +571,7 @@ main {
font-size: 6rem; font-size: 6rem;
font-weight: 800; font-weight: 800;
color: var(--error); color: var(--error);
font-family: "Consolar", "Monaco", monospace; font-family: "Consolas", "Monaco", monospace;
opacity: 0.8; opacity: 0.8;
line-height: 1; line-height: 1;
margin-bottom: 1rem; margin-bottom: 1rem;
@ -584,3 +606,68 @@ main {
width: auto; width: auto;
text-align: center; text-align: center;
} }
.input-wrapper {
width: 100%;
&.hex-input {
@include input-styles;
display: flex;
align-items: center;
padding: 0;
cursor: text;
.prefix {
padding-left: 12px;
padding-right: 2px;
color: var(--text-muted);
user-select: none;
font-weight: bold;
opacity: 0.8;
}
input[type="text"] {
border: none;
background: transparent;
box-shadow: none;
outline: none;
margin: 0;
width: 100%;
padding: 12px 12px 12px 0px;
&:focus {
outline: none;
box-shadow: none;
border: none;
}
}
}
&.standard-input {
input[type="text"] {
width: 100%;
}
}
}
.label-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.5rem;
label {
margin-bottom: 0;
}
}
.input-hint {
font-size: 0.75rem;
color: var(--text-muted);
opacity: 0.7;
font-family: "Inter", "Segoe UI", sans-serif;
text-transform: none;
letter-spacing: normal;
}