Skip to content

Commit 6d8d452

Browse files
authored
Merge pull request #159 from CodeDead/feature/refactoring
feat: refactoring
2 parents 394d38e + edc8d6f commit 6d8d452

9 files changed

Lines changed: 428 additions & 313 deletions

File tree

src-tauri/Cargo.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ rust-version = "1.95.0"
1515
tauri-build = { version = "2.5.6", features = [] }
1616

1717
[dependencies]
18-
serde_json = "1.0"
19-
serde = { version = "1.0", features = ["derive"] }
2018
tauri = { version = "2.10.3", features = [] }
2119
open = "5.3.3"
2220
rand = "0.10.1"

src-tauri/src/main.rs

Lines changed: 93 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,52 @@
44
)]
55

66
use rand::RngExt;
7-
use std::fs;
7+
use std::{collections::HashSet, fmt::Display, fs};
88
use unicode_segmentation::UnicodeSegmentation;
99

10-
fn main() {
11-
#[cfg(target_os = "linux")]
10+
type CommandResult<T> = Result<T, String>;
11+
12+
#[cfg(target_os = "linux")]
13+
fn configure_linux_environment() {
14+
// SAFETY: This runs during application startup before any threads are spawned
15+
// and only sets a fixed process-wide environment variable needed on Linux.
1216
unsafe {
1317
std::env::set_var("__NV_DISABLE_EXPLICIT_SYNC", "1");
1418
}
19+
}
20+
21+
fn map_command_result<T, E>(result: Result<T, E>) -> CommandResult<T>
22+
where
23+
E: Display,
24+
{
25+
result.map_err(|error| error.to_string())
26+
}
27+
28+
fn as_platform_size(value: u64, field_name: &str) -> CommandResult<usize> {
29+
usize::try_from(value).map_err(|_| format!("{field_name} is too large for this platform."))
30+
}
31+
32+
fn count_unique_passwords(min_length: usize, max_length: usize, grapheme_count: usize) -> usize {
33+
let mut total = 0usize;
34+
35+
for length in min_length..=max_length {
36+
let combinations = u32::try_from(length)
37+
.ok()
38+
.and_then(|value| grapheme_count.checked_pow(value))
39+
.unwrap_or(usize::MAX);
40+
41+
total = total.saturating_add(combinations);
42+
if total == usize::MAX {
43+
break;
44+
}
45+
}
46+
47+
total
48+
}
49+
50+
fn main() {
51+
#[cfg(target_os = "linux")]
52+
configure_linux_environment();
1553

1654
tauri::Builder::default()
1755
.plugin(tauri_plugin_os::init())
@@ -34,76 +72,83 @@ fn exit_app() {
3472
}
3573

3674
#[tauri::command]
37-
fn open_website(website: &str) -> Result<String, String> {
38-
match open::that(website) {
39-
Ok(_) => Ok(String::from("Success")),
40-
Err(e) => Err(e.to_string()),
41-
}
75+
fn open_website(website: &str) -> CommandResult<()> {
76+
map_command_result(open::that(website))
4277
}
4378

4479
#[tauri::command]
45-
fn save_string_to_disk(content: &str, path: &str) -> Result<String, String> {
46-
match fs::write(path, content) {
47-
Ok(_) => Ok(String::from("Success")),
48-
Err(e) => Err(e.to_string()),
49-
}
80+
fn save_string_to_disk(content: &str, path: &str) -> CommandResult<()> {
81+
map_command_result(fs::write(path, content))
5082
}
5183

5284
#[tauri::command]
53-
async fn read_string_from_file(path: &str) -> Result<String, String> {
54-
match fs::read_to_string(path) {
55-
Ok(s) => Ok(s),
56-
Err(e) => Err(e.to_string()),
57-
}
85+
fn read_string_from_file(path: &str) -> CommandResult<String> {
86+
map_command_result(fs::read_to_string(path))
5887
}
5988

6089
#[tauri::command]
61-
async fn generate_passwords(
90+
fn generate_passwords(
6291
min_length: u64,
6392
max_length: u64,
6493
character_set: &str,
6594
include_symbols: &str,
6695
amount: u64,
6796
allow_duplicates: bool,
68-
) -> Result<Vec<String>, String> {
69-
let mut password_list: Vec<String> = Vec::new();
70-
let mut max_count: f64 = 0.0;
97+
) -> CommandResult<Vec<String>> {
98+
let min_length = as_platform_size(min_length, "Minimum length")?;
99+
let max_length = as_platform_size(max_length, "Maximum length")?;
100+
let requested_amount = as_platform_size(amount, "Password amount")?;
71101

72-
let mut total_character_set = String::from(character_set);
73-
total_character_set.push_str(include_symbols);
102+
if min_length > max_length {
103+
return Err("Minimum length cannot be greater than maximum length.".into());
104+
}
105+
106+
if requested_amount == 0 {
107+
return Ok(Vec::new());
108+
}
74109

75-
let graphemes = total_character_set.graphemes(true);
76-
let char_count = graphemes.clone().count();
110+
let total_character_set = format!("{character_set}{include_symbols}");
111+
let graphemes = total_character_set.graphemes(true).collect::<Vec<&str>>();
112+
let grapheme_count = graphemes.len();
77113

78-
if !allow_duplicates {
79-
let mut current = min_length;
80-
while current <= max_length {
81-
max_count += (char_count as f64).powf(current as f64);
82-
current += 1;
83-
}
114+
if grapheme_count == 0 {
115+
return Ok(Vec::new());
84116
}
85117

118+
let target_amount = if allow_duplicates {
119+
requested_amount
120+
} else {
121+
requested_amount.min(count_unique_passwords(
122+
min_length,
123+
max_length,
124+
grapheme_count,
125+
))
126+
};
127+
128+
if target_amount == 0 {
129+
return Ok(Vec::new());
130+
}
131+
132+
let mut password_list = Vec::with_capacity(target_amount);
133+
let mut seen_passwords = (!allow_duplicates).then(|| HashSet::with_capacity(target_amount));
86134
let mut rng = rand::rng();
87-
let chars = graphemes.collect::<Vec<&str>>();
88-
for _n in 0..amount {
89-
let mut can_continue = false;
90-
while !can_continue {
91-
let mut password = String::from("");
92-
let length = rng.random_range(min_length..(max_length + 1));
93-
for _j in 0..length {
94-
let index = rng.random_range(0..char_count);
95-
password.push_str(chars.clone()[index]);
96-
}
97135

98-
if allow_duplicates || !password_list.contains(&password) {
99-
password_list.push(password);
100-
can_continue = true;
101-
}
136+
while password_list.len() < target_amount {
137+
let length = rng.random_range(min_length..=max_length);
138+
let mut password = String::new();
139+
140+
for _ in 0..length {
141+
let index = rng.random_range(0..grapheme_count);
142+
password.push_str(graphemes[index]);
143+
}
102144

103-
if !can_continue && !allow_duplicates && password_list.len() as f64 == max_count {
104-
return Ok(password_list);
145+
if let Some(seen_passwords) = seen_passwords.as_mut() {
146+
if !seen_passwords.insert(password.clone()) {
147+
continue;
105148
}
106149
}
150+
151+
password_list.push(password);
107152
}
108153

109154
Ok(password_list)

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
}
4141
}
4242
},
43-
"productName": "advanced-passgen",
43+
"productName": "Advanced PassGen",
4444
"mainBinaryName": "advanced-passgen",
4545
"version": "2.5.2",
4646
"identifier": "com.codedead.advancedpassgen",

src/components/PasswordTips/index.jsx

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,85 @@
1-
import React, { useContext, useEffect, useRef, useState } from 'react';
1+
import React, {
2+
useCallback,
3+
useContext,
4+
useEffect,
5+
useRef,
6+
useState,
7+
} from 'react';
28
import CloseIcon from '@mui/icons-material/Close';
39
import Alert from '@mui/material/Alert';
410
import AlertTitle from '@mui/material/AlertTitle';
511
import Collapse from '@mui/material/Collapse';
612
import IconButton from '@mui/material/IconButton';
713
import { MainContext } from '../../contexts/MainContextProvider';
814

15+
const getRandomTipIndex = (length) =>
16+
length > 0 ? Math.floor(Math.random() * length) : 0;
17+
918
const PasswordTips = () => {
1019
const [state] = useContext(MainContext);
1120

1221
const { languageIndex, tips } = state;
1322
const language = state.languages[languageIndex];
1423

1524
const [tipsOpen, setTipsOpen] = useState(tips);
16-
const intervalId = useRef();
17-
const [currentTip, setCurrentTip] = useState(
18-
language.passwordTips[
19-
// eslint-disable-next-line react-hooks/purity
20-
Math.floor(Math.random() * language.passwordTips.length)
21-
],
25+
const intervalId = useRef(null);
26+
const [tipIndex, setTipIndex] = useState(() =>
27+
getRandomTipIndex(language.passwordTips.length),
2228
);
29+
const currentTip =
30+
language.passwordTips[tipIndex % language.passwordTips.length] ?? '';
2331

2432
/**
25-
* Generate a new tipper
33+
* Clear the active tip timer
2634
*/
27-
const generateNewTipper = () => {
35+
const clearTipInterval = useCallback(() => {
2836
if (intervalId.current !== null) {
2937
clearInterval(intervalId.current);
38+
intervalId.current = null;
3039
}
40+
}, []);
3141

32-
intervalId.current = setInterval(() => {
33-
setCurrentTip(
34-
language.passwordTips[
35-
Math.floor(Math.random() * language.passwordTips.length)
36-
],
37-
);
38-
}, 30000);
39-
};
42+
/**
43+
* Start rotating the current tip
44+
* @param passwordTips The available tips for the current language
45+
*/
46+
const startTipRotation = useCallback(
47+
(passwordTips) => {
48+
clearTipInterval();
4049

41-
useEffect(() => {
42-
if (tips) {
43-
generateNewTipper();
44-
}
50+
if (!passwordTips || passwordTips.length === 0) {
51+
return;
52+
}
4553

46-
return () =>
47-
intervalId !== null ? clearInterval(intervalId.current) : null;
48-
// eslint-disable-next-line react-hooks/exhaustive-deps
49-
}, []);
54+
intervalId.current = setInterval(() => {
55+
setTipIndex(getRandomTipIndex(passwordTips.length));
56+
}, 30000);
57+
},
58+
[clearTipInterval],
59+
);
5060

5161
useEffect(() => {
52-
if (tips) {
53-
// eslint-disable-next-line react-hooks/set-state-in-effect
54-
setCurrentTip(
55-
language.passwordTips[
56-
Math.floor(Math.random() * language.passwordTips.length)
57-
],
58-
);
62+
if (!tips) {
63+
clearTipInterval();
64+
return clearTipInterval;
5965
}
60-
// eslint-disable-next-line react-hooks/exhaustive-deps
61-
}, [language]);
6266

63-
useEffect(() => {
64-
if (tips) {
65-
generateNewTipper();
66-
} else {
67-
clearInterval(intervalId.current);
68-
}
69-
// eslint-disable-next-line react-hooks/exhaustive-deps
70-
}, [tips]);
67+
startTipRotation(language.passwordTips);
68+
69+
return clearTipInterval;
70+
}, [clearTipInterval, language.passwordTips, startTipRotation, tips]);
71+
72+
const closeTips = () => {
73+
setTipsOpen(false);
74+
clearTipInterval();
75+
};
7176

7277
return (
73-
<Collapse in={tipsOpen}>
78+
<Collapse in={tips && tipsOpen}>
7479
<Alert
7580
severity="info"
7681
action={
77-
<IconButton
78-
aria-label="close"
79-
color="inherit"
80-
onClick={() => {
81-
setTipsOpen(false);
82-
clearInterval(intervalId.current);
83-
}}
84-
>
82+
<IconButton aria-label="close" color="inherit" onClick={closeTips}>
8583
<CloseIcon fontSize="inherit" />
8684
</IconButton>
8785
}

src/reducers/MainReducer/Actions/index.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,30 @@ import {
1616
SET_UPDATE,
1717
} from './actionTypes';
1818

19+
const normalizeErrorMessage = (error) => {
20+
if (error === null || error === undefined) {
21+
return null;
22+
}
23+
24+
if (typeof error === 'string') {
25+
return error;
26+
}
27+
28+
if (error instanceof Error) {
29+
return error.message;
30+
}
31+
32+
if (
33+
typeof error === 'object' &&
34+
'message' in error &&
35+
typeof error.message === 'string'
36+
) {
37+
return error.message;
38+
}
39+
40+
return String(error);
41+
};
42+
1943
export const setLanguageIndex = (index) => ({
2044
type: SET_LANGUAGE_INDEX,
2145
payload: index,
@@ -62,7 +86,7 @@ export const setUpdate = (update) => ({
6286

6387
export const setError = (error) => ({
6488
type: SET_ERROR,
65-
payload: error,
89+
payload: normalizeErrorMessage(error),
6690
});
6791

6892
export const setLanguageSelector = (value) => ({

0 commit comments

Comments
 (0)