mirror of
https://github.com/XFox111/PasswordGeneratorExtension.git
synced 2026-04-22 08:08:01 +03:00
Major 5.0 (#469)
* Advanced generator, UI overhaul (#449) * Major overhaul: - Added advanced generator - Removed "Insert and copy" feature - Moved settings to extension options - General refactoring * Updated custom character options for default generator (#447) * Added state save for advanced generator mode * Fixed state save for advanced password generator * Updated extension description * Minor UI fixes: - Fixed Options UI not displaying in Google Chrome - Fixed Quick Options menus overflowing on some locales - Fixed Advanced generator configuration UI clipping when window height is too small - Fixed divider in Options UI taking up all available space * Minor UI/UX changes and fixes: - Fixed locale in Advanced generator toast notifications - Added toast notification for copying a single password in Advanced generator - Moved custom characters input lables to placeholders - Fixed minor type issues - Removed duplicate "About" text - Fixed input fields alignment in Options - Added "disabled" state for "Include Custom" option in Quick settings * Bump @typescript-eslint/parser from 8.16.0 to 8.19.1 (#468) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.16.0 to 8.19.1. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.19.1/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @eslint/js from 9.16.0 to 9.18.0 (#467) Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.16.0 to 9.18.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/commits/v9.18.0/packages/js) --- updated-dependencies: - dependency-name: "@eslint/js" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @fluentui/react-components from 9.56.3 to 9.57.0 (#466) Bumps [@fluentui/react-components](https://github.com/microsoft/fluentui) from 9.56.3 to 9.57.0. - [Release notes](https://github.com/microsoft/fluentui/releases) - [Changelog](https://github.com/microsoft/fluentui/blob/master/azure-pipelines.release.yml) - [Commits](https://github.com/microsoft/fluentui/compare/@fluentui/react-components_v9.56.3...@fluentui/react-components_v9.57.0) --- updated-dependencies: - dependency-name: "@fluentui/react-components" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump wxt from 0.19.17 to 0.19.24 (#465) Bumps [wxt](https://github.com/wxt-dev/wxt) from 0.19.17 to 0.19.24. - [Release notes](https://github.com/wxt-dev/wxt/releases) - [Commits](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.17...wxt-v0.19.24) --- updated-dependencies: - dependency-name: wxt dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump eslint from 9.16.0 to 9.18.0 (#464) Bumps [eslint](https://github.com/eslint/eslint) from 9.16.0 to 9.18.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.16.0...v9.18.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump eslint-plugin-react from 7.37.2 to 7.37.4 (#463) Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.37.2 to 7.37.4. - [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases) - [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md) - [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.37.2...v7.37.4) --- updated-dependencies: - dependency-name: eslint-plugin-react dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene Fox <eugene.xfox@outlook.com> * Bump typescript from 5.7.2 to 5.7.3 (#462) Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.7.2 to 5.7.3. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.2...v5.7.3) --- updated-dependencies: - dependency-name: typescript dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene Fox <eugene.xfox@outlook.com> * Bump @typescript-eslint/eslint-plugin from 8.16.0 to 8.19.1 (#461) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.16.0 to 8.19.1. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.19.1/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump wdzeng/chrome-extension from 1.2.4 to 1.3.0 (#450) * Bump wdzeng/chrome-extension from 1.2.4 to 1.3.0 Bumps [wdzeng/chrome-extension](https://github.com/wdzeng/chrome-extension) from 1.2.4 to 1.3.0. - [Release notes](https://github.com/wdzeng/chrome-extension/releases) - [Commits](https://github.com/wdzeng/chrome-extension/compare/v1.2.4...v1.3.0) --- updated-dependencies: - dependency-name: wdzeng/chrome-extension dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * Optimized CD workflow --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene Fox <eugene@xfox111.net> * Bump @fluentui/react-icons from 2.0.266 to 2.0.270 (#458) Bumps [@fluentui/react-icons](https://github.com/microsoft/fluentui-system-icons) from 2.0.266 to 2.0.270. - [Changelog](https://github.com/microsoft/fluentui-system-icons/blob/main/fluentui-android-system-icons-release.yml) - [Commits](https://github.com/microsoft/fluentui-system-icons/commits) --- updated-dependencies: - dependency-name: "@fluentui/react-icons" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @wxt-dev/module-react from 1.1.2 to 1.1.3 (#455) Bumps [@wxt-dev/module-react](https://github.com/wxt-dev/wxt/tree/HEAD/packages/module-react) from 1.1.2 to 1.1.3. - [Release notes](https://github.com/wxt-dev/wxt/releases) - [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/module-react/CHANGELOG.md) - [Commits](https://github.com/wxt-dev/wxt/commits/module-react-v1.1.3/packages/module-react) --- updated-dependencies: - dependency-name: "@wxt-dev/module-react" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump globals from 15.12.0 to 15.14.0 (#452) Bumps [globals](https://github.com/sindresorhus/globals) from 15.12.0 to 15.14.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v15.12.0...v15.14.0) --- updated-dependencies: - dependency-name: globals dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
height: "100vh",
|
||||
gap: tokens.spacingHorizontalXL,
|
||||
padding: `${tokens.spacingVerticalXL} ${tokens.spacingHorizontalXL}`,
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
smallRoot:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column-reverse",
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalS}`,
|
||||
},
|
||||
hideScroll:
|
||||
{
|
||||
overflowY: "visible",
|
||||
maxHeight: "unset",
|
||||
},
|
||||
configRoot:
|
||||
{
|
||||
maxHeight: "90vh",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalM,
|
||||
width: "100%",
|
||||
maxWidth: "480px",
|
||||
borderRadius: tokens.borderRadiusLarge,
|
||||
boxShadow: tokens.shadow4,
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
|
||||
margin: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
|
||||
boxSizing: "border-box",
|
||||
justifySelf: "center",
|
||||
alignSelf: "center",
|
||||
overflowY: "auto",
|
||||
},
|
||||
switch:
|
||||
{
|
||||
margin: `${tokens.spacingVerticalXL} ${tokens.spacingVerticalNone}`,
|
||||
},
|
||||
listRoot:
|
||||
{
|
||||
width: "100%",
|
||||
maxWidth: "840px",
|
||||
alignSelf: "center",
|
||||
justifySelf: "center",
|
||||
padding: `${tokens.spacingVerticalXL} ${tokens.spacingHorizontalXL}`,
|
||||
boxSizing: "border-box",
|
||||
overflowY: "auto",
|
||||
maxHeight: "100%",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import DoubleLabledSwitch from "@/shared/DoubleLabeledSwitch";
|
||||
import { mergeClasses, Toaster } from "@fluentui/react-components";
|
||||
import { ReactElement } from "react";
|
||||
import { useMediaQuery } from "react-responsive";
|
||||
import PasswordList from "./components/PasswordList";
|
||||
import { useStyles } from "./Page.styles";
|
||||
import PassphraseSection from "./sections/PassphraseSection";
|
||||
import PasswordSection from "./sections/PasswordSection";
|
||||
|
||||
export default function Page(): ReactElement
|
||||
{
|
||||
const [isPassphrase, setIsPassphrase] = useState<boolean | null>(null);
|
||||
const [passwords, setPasswords] = useState<string[]>([]);
|
||||
const isSmall = useMediaQuery({ query: "(max-width: 1000px)" });
|
||||
|
||||
const cls = useStyles();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
advancedPassphraseSelected.getValue().then(setIsPassphrase);
|
||||
const unwatch = advancedPassphraseSelected.watch(setIsPassphrase);
|
||||
|
||||
return () => unwatch();
|
||||
}, []);
|
||||
|
||||
if (isPassphrase === null)
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<main className={ mergeClasses(cls.root, isSmall && cls.smallRoot) }>
|
||||
<PasswordList
|
||||
passwords={ passwords }
|
||||
className={ mergeClasses(cls.listRoot, isSmall && cls.hideScroll) } />
|
||||
|
||||
<article className={ mergeClasses(cls.configRoot, isSmall && cls.hideScroll) }>
|
||||
<DoubleLabledSwitch outerRoot={ { className: cls.switch } }
|
||||
checked={ isPassphrase }
|
||||
onChange={ (_, e) => advancedPassphraseSelected.setValue(e.checked) }
|
||||
offLabel={ i18n.t("advanced.password.title") }
|
||||
onLabel={ i18n.t("advanced.passphrase.title") } />
|
||||
|
||||
{ isPassphrase ?
|
||||
<PassphraseSection onGenerated={ setPasswords } />
|
||||
:
|
||||
<PasswordSection onGenerated={ setPasswords } />
|
||||
}
|
||||
</article>
|
||||
<Toaster />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export type GeneratorProps =
|
||||
{
|
||||
onGenerated: (passwords: string[]) => void;
|
||||
};
|
||||
|
||||
const advancedPassphraseSelected = storage.defineItem<boolean>("sync:AdvancedPassphraseSelected", { fallback: false });
|
||||
@@ -0,0 +1,22 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalXL,
|
||||
},
|
||||
actionRoot:
|
||||
{
|
||||
display: "flex",
|
||||
gap: tokens.spacingHorizontalM,
|
||||
alignSelf: "center",
|
||||
},
|
||||
bulkRoot:
|
||||
{
|
||||
display: "grid",
|
||||
alignItems: "center",
|
||||
gridTemplateColumns: "24px 24px 56px",
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Button, Input, InputOnChangeData, MessageBar, MessageBarBody, MessageBarTitle, Text, Toast, ToastTitle, useToastController } from "@fluentui/react-components";
|
||||
import { bundleIcon, Key24Regular, Save20Filled, Save20Regular } from "@fluentui/react-icons";
|
||||
import { PropsWithChildren, ReactElement } from "react";
|
||||
import { useStyles } from "./GeneratorForm.styles";
|
||||
|
||||
export default function GeneratorForm(props: GeneratorFormProps): ReactElement
|
||||
{
|
||||
const [passwordCount, private_setPasswordCount] = useState<number | null>(5);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const toaster = useToastController();
|
||||
|
||||
const cls = useStyles();
|
||||
const SaveIcon = bundleIcon(Save20Filled, Save20Regular);
|
||||
|
||||
const setPasswordCount = useCallback((_: any, e: InputOnChangeData) =>
|
||||
{
|
||||
const n = parseInt(e.value ?? "1");
|
||||
private_setPasswordCount(isNaN(n) || n < 1 ? null : Math.min(n, 1000));
|
||||
}, []);
|
||||
|
||||
const onSubmit = useCallback((args: React.FormEvent<HTMLFormElement>) =>
|
||||
{
|
||||
args.preventDefault();
|
||||
|
||||
try
|
||||
{
|
||||
setError(null);
|
||||
props.onGenerate(passwordCount ?? 1);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
setError((ex as Error).message);
|
||||
}
|
||||
}, [props.onGenerate, passwordCount]);
|
||||
|
||||
const onSave = useCallback(async () =>
|
||||
{
|
||||
props.onSave();
|
||||
await browser.storage.sync.set({ AdvancedBulkCount: passwordCount ?? 5 });
|
||||
|
||||
toaster.dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>{ i18n.t("advanced.saved_msg") }</ToastTitle>
|
||||
</Toast>,
|
||||
{
|
||||
intent: "success",
|
||||
timeout: 1000
|
||||
}
|
||||
);
|
||||
}, [props.onSave, toaster, passwordCount]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
browser.storage.sync.get("AdvancedBulkCount").then(({ AdvancedBulkCount }) =>
|
||||
private_setPasswordCount(AdvancedBulkCount as number ?? 5)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form onSubmit={ onSubmit } className={ cls.root }>
|
||||
{ props.children }
|
||||
|
||||
{ error &&
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>{ error }</MessageBarTitle>
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
}
|
||||
|
||||
<div className={ cls.actionRoot }>
|
||||
<div className={ cls.bulkRoot }>
|
||||
<Key24Regular />
|
||||
<Text align="center">x</Text>
|
||||
<Input value={ passwordCount?.toString() ?? "" } onChange={ setPasswordCount } />
|
||||
</div>
|
||||
<Button appearance="primary" type="submit">{ i18n.t("advanced.actions.generate") }</Button>
|
||||
</div>
|
||||
|
||||
<Button appearance="subtle" icon={ <SaveIcon /> } onClick={ onSave }>
|
||||
{ i18n.t("advanced.actions.save_config") }
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export type GeneratorFormProps = PropsWithChildren &
|
||||
{
|
||||
onSave: () => void;
|
||||
onGenerate: (count: number) => void;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalS,
|
||||
},
|
||||
copyAll:
|
||||
{
|
||||
alignSelf: "flex-end",
|
||||
minHeight: "32px",
|
||||
},
|
||||
table:
|
||||
{
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
borderRadius: "16px",
|
||||
overflow: "clip",
|
||||
},
|
||||
row:
|
||||
{
|
||||
"&:last-child":
|
||||
{
|
||||
borderBottom: "none",
|
||||
}
|
||||
},
|
||||
cell:
|
||||
{
|
||||
padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
|
||||
cursor: "pointer",
|
||||
},
|
||||
cellLayout:
|
||||
{
|
||||
overflowX: "auto",
|
||||
},
|
||||
passwordText:
|
||||
{
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
overflowX: "hidden",
|
||||
display: "block",
|
||||
marginRight: "36px",
|
||||
},
|
||||
copyIcon:
|
||||
{
|
||||
verticalAlign: "middle",
|
||||
padding: tokens.spacingHorizontalXL,
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { bundleIcon, Copy32Regular, CopyFilled, CopyRegular } from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import { useStyles } from "./PasswordList.styles";
|
||||
|
||||
export default function PasswordList({ passwords, className }: PasswordListProps): ReactElement
|
||||
{
|
||||
const toaster = fui.useToastController();
|
||||
|
||||
const CopyIcon = bundleIcon(CopyFilled, CopyRegular);
|
||||
const cls = useStyles();
|
||||
|
||||
const copy = (password?: string) =>
|
||||
{
|
||||
navigator.clipboard.writeText(password ?? passwords.join("\n"));
|
||||
toaster.dispatchToast(
|
||||
<fui.Toast>
|
||||
<fui.ToastTitle>{ i18n.t("advanced.copied_msg") }</fui.ToastTitle>
|
||||
</fui.Toast>,
|
||||
{
|
||||
intent: "success",
|
||||
timeout: 1000
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ fui.mergeClasses(cls.root, className) }>
|
||||
{ passwords.length > 0 &&
|
||||
<>
|
||||
<fui.Button className={ cls.copyAll }
|
||||
appearance="subtle" icon={ <CopyIcon /> }
|
||||
onClick={ () => copy() }>
|
||||
|
||||
{ i18n.t("advanced.actions.copy_all") }
|
||||
</fui.Button>
|
||||
|
||||
<fui.Table className={ cls.table }>
|
||||
<fui.TableBody>
|
||||
|
||||
{ passwords.map((password, index) =>
|
||||
<fui.TableRow key={ index } className={ cls.row }
|
||||
onClick={ () => copy(password) }>
|
||||
|
||||
<fui.TableCell className={ cls.cell }>
|
||||
|
||||
<fui.TableCellLayout content={ { className: cls.cellLayout } }>
|
||||
<fui.Tooltip relationship="description" content={ password }>
|
||||
|
||||
<fui.Text className={ cls.passwordText }
|
||||
font="monospace" size={ 600 }>
|
||||
|
||||
{ password }
|
||||
</fui.Text>
|
||||
|
||||
</fui.Tooltip>
|
||||
</fui.TableCellLayout>
|
||||
|
||||
<fui.TableCellActions>
|
||||
<Copy32Regular className={ cls.copyIcon } />
|
||||
</fui.TableCellActions>
|
||||
|
||||
</fui.TableCell>
|
||||
</fui.TableRow>
|
||||
) }
|
||||
|
||||
</fui.TableBody>
|
||||
</fui.Table>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type PasswordListProps =
|
||||
{
|
||||
className?: string;
|
||||
passwords: string[];
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Advanced password generator</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,51 @@
|
||||
html,
|
||||
body,
|
||||
#root > .fui-FluentProvider
|
||||
{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
p, h1, h2
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar
|
||||
{
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track
|
||||
{
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb
|
||||
{
|
||||
border-radius: 4px;
|
||||
background: light-dark(#d1d1d1, #666666);
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover
|
||||
{
|
||||
background: light-dark(#c7c7c7, #757575);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover:active
|
||||
{
|
||||
background: light-dark(#b3b3b3, #6b6b6b);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "../../shared/App";
|
||||
import "./main.css";
|
||||
import Page from "./Page";
|
||||
|
||||
document.title = i18n.t("advanced.title");
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App>
|
||||
<Page />
|
||||
</App>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalSNudge,
|
||||
},
|
||||
checkboxes:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalXXS,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import generatePassphrase, { PassphraseProps } from "@/utils/generators/generatePassphrase";
|
||||
import infoLabel from "@/utils/infoLabel";
|
||||
import { Checkbox, Field, Input, InputOnChangeData } from "@fluentui/react-components";
|
||||
import { ReactElement } from "react";
|
||||
import GeneratorForm from "../components/GeneratorForm";
|
||||
import { GeneratorProps } from "../Page";
|
||||
import { useStyles } from "./PassphraseSection.styles";
|
||||
|
||||
export default function PassphraseSection(props: GeneratorProps): ReactElement
|
||||
{
|
||||
const [wordCount, private_setWordCount] = useState<number | null>(2);
|
||||
const [swapCharacters, setSwapCharacters] = useState<boolean>(false);
|
||||
const [separate, setSeparate] = useState<boolean>(true);
|
||||
const [separator, setSeparator] = useState<string>("");
|
||||
const [allowRepeating, setAllowRepeating] = useState<boolean>(false);
|
||||
const [randomizeCase, setRandomizeCase] = useState<boolean>(false);
|
||||
|
||||
const config = useMemo<PassphraseProps>(() => ({
|
||||
wordCount: wordCount ?? 2,
|
||||
allowRepeating,
|
||||
swapCharacters,
|
||||
randomizeCase,
|
||||
separator: separate ? (separator ? separator : " ") : ""
|
||||
}), [wordCount, allowRepeating, swapCharacters, randomizeCase, separate, separator]);
|
||||
|
||||
const cls = useStyles();
|
||||
|
||||
const setWordCount = useCallback((_: any, e: InputOnChangeData) =>
|
||||
{
|
||||
const n = parseInt(e.value ?? "");
|
||||
private_setWordCount(isNaN(n) || n < 1 ? null : Math.min(n, 100));
|
||||
}, []);
|
||||
|
||||
const saveConfiguration = useCallback(
|
||||
async () => await browser.storage.sync.set({ AdvancedPassphraseOptions: config }),
|
||||
[config]
|
||||
);
|
||||
|
||||
const generate = useCallback((count: number) =>
|
||||
{
|
||||
const passwords: string[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++)
|
||||
passwords.push(generatePassphrase(config));
|
||||
|
||||
props.onGenerated(passwords);
|
||||
}, [config, props.onGenerated]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
browser.storage.sync.get("AdvancedPassphraseOptions").then(({ AdvancedPassphraseOptions }) =>
|
||||
{
|
||||
if (!AdvancedPassphraseOptions)
|
||||
return;
|
||||
|
||||
private_setWordCount(AdvancedPassphraseOptions.wordCount ?? 2);
|
||||
setAllowRepeating(AdvancedPassphraseOptions.allowRepeating);
|
||||
setSwapCharacters(AdvancedPassphraseOptions.swapCharacters);
|
||||
setRandomizeCase(AdvancedPassphraseOptions.randomizeCase);
|
||||
setSeparate(!!AdvancedPassphraseOptions.separator);
|
||||
setSeparator(AdvancedPassphraseOptions.separator ?? "");
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GeneratorForm onGenerate={ generate } onSave={ saveConfiguration }>
|
||||
<div className={ cls.root }>
|
||||
<Field label={ i18n.t("advanced.passphrase.length") }>
|
||||
<Input value={ wordCount?.toString() ?? "" } onChange={ setWordCount } />
|
||||
</Field>
|
||||
<div className={ cls.checkboxes }>
|
||||
<Checkbox label={ i18n.t("advanced.passphrase.replace") }
|
||||
checked={ swapCharacters } onChange={ (_, e) => setSwapCharacters(e.checked as boolean) } />
|
||||
<Checkbox label={ i18n.t("advanced.passphrase.random_case") }
|
||||
checked={ randomizeCase } onChange={ (_, e) => setRandomizeCase(e.checked as boolean) } />
|
||||
<Checkbox label={ infoLabel(i18n.t("advanced.passphrase.allow_repat.label"), i18n.t("advanced.passphrase.allow_repat.hint")) }
|
||||
checked={ allowRepeating } onChange={ (_, e) => setAllowRepeating(e.checked as boolean) } />
|
||||
|
||||
<div>
|
||||
<Checkbox label={ infoLabel(i18n.t("advanced.passphrase.separate_words.label"), i18n.t("advanced.passphrase.separate_words.hint")) }
|
||||
checked={ separate } onChange={ (_, e) => setSeparate(e.checked as boolean) } />
|
||||
<Input disabled={ !separate } size="small"
|
||||
value={ separator } onChange={ (_, e) => setSeparator(e.value) } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GeneratorForm>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { CharacterHints, generatePassword } from "@/utils/generators/generatePassword";
|
||||
import infoLabel from "@/utils/infoLabel";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { ReactElement } from "react";
|
||||
import { GeneratorProps } from "../Page";
|
||||
import GeneratorForm from "../components/GeneratorForm";
|
||||
|
||||
// TODO: needs refactoring
|
||||
export default function PasswordSection(props: GeneratorProps): ReactElement
|
||||
{
|
||||
const [state, private_setState] = useState<PasswordSectionState>({
|
||||
length: 8,
|
||||
enableUppercase: true, uppercaseCount: 1,
|
||||
enableLowercase: true, lowercaseCount: 1,
|
||||
enableNumeric: true, numericCount: 1,
|
||||
enableSpecial: true, specialCount: 1,
|
||||
enableCustom: false, customCount: 1, customSet: "",
|
||||
|
||||
excludeSimilar: true,
|
||||
excludeAmbiguous: true,
|
||||
excludeRepeating: false,
|
||||
excludeCustom: false, excludeCustomSet: ""
|
||||
});
|
||||
|
||||
const cls = useStyles();
|
||||
|
||||
const setState = useCallback((newState: Partial<PasswordSectionState>) =>
|
||||
{
|
||||
private_setState({ ...state, ...newState });
|
||||
}, [state]);
|
||||
|
||||
const setLength = useCallback((_: any, e: fui.InputOnChangeData) =>
|
||||
{
|
||||
const n = parseInt(e.value ?? "");
|
||||
setState({ length: isNaN(n) || n < 1 ? null : n });
|
||||
}, [state]);
|
||||
|
||||
const saveConfiguration = useCallback(
|
||||
async () => await browser.storage.sync.set({ AdvancedPasswordOptions: state }),
|
||||
[state]
|
||||
);
|
||||
|
||||
const generate = useCallback((count: number) =>
|
||||
{
|
||||
const passwords: string[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++)
|
||||
passwords.push(generatePassword({
|
||||
length: state.length ?? 8,
|
||||
custom: state.enableCustom ? state.customCount ?? 1 : 0,
|
||||
customSet: state.customSet,
|
||||
numeric: state.enableNumeric ? state.numericCount ?? 1 : 0,
|
||||
special: state.enableSpecial ? state.specialCount ?? 1 : 0,
|
||||
uppercase: state.enableUppercase ? state.uppercaseCount ?? 1 : 0,
|
||||
lowercase: state.enableLowercase ? state.lowercaseCount ?? 1 : 0,
|
||||
excludeAmbiguous: state.excludeAmbiguous,
|
||||
excludeCustom: state.excludeCustom ? state.excludeCustomSet : "",
|
||||
excludeRepeating: state.excludeRepeating,
|
||||
excludeSimilar: state.excludeSimilar,
|
||||
}));
|
||||
|
||||
props.onGenerated(passwords);
|
||||
}, [state, props.onGenerated]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
browser.storage.sync.get("AdvancedPasswordOptions").then(({ AdvancedPasswordOptions }) =>
|
||||
private_setState({ ...state, ...AdvancedPasswordOptions as PasswordSectionState }));
|
||||
}, []);
|
||||
|
||||
const checkboxControls = useCallback((key: keyof PasswordSectionState): Partial<fui.CheckboxProps> => ({
|
||||
checked: state[key] as boolean,
|
||||
onChange: (_, e) => setState({ [key]: e.checked as boolean })
|
||||
}), [state]);
|
||||
|
||||
const minInputControls = useCallback((enabledKey: keyof PasswordSectionState, key: keyof PasswordSectionState): Partial<fui.InputProps> => ({
|
||||
size: "small",
|
||||
disabled: !state[enabledKey],
|
||||
value: state[key]?.toString() ?? "",
|
||||
onChange: (_, e) => setState({ [key]: parseCount(e.value) })
|
||||
}), [state]);
|
||||
|
||||
return (
|
||||
<GeneratorForm onGenerate={ generate } onSave={ saveConfiguration }>
|
||||
<fui.Field label={ i18n.t("advanced.password.length") }>
|
||||
<fui.Input value={ state.length?.toString() ?? "" } onChange={ setLength } />
|
||||
</fui.Field>
|
||||
<fui.Table size="small" as="div">
|
||||
<fui.TableHeader as="div">
|
||||
<fui.TableRow as="div">
|
||||
<fui.TableHeaderCell as="div">{ i18n.t("common.sections.include") }</fui.TableHeaderCell>
|
||||
<fui.TableHeaderCell as="div">{ i18n.t("advanced.password.min_of_type") }</fui.TableHeaderCell>
|
||||
</fui.TableRow>
|
||||
</fui.TableHeader>
|
||||
<fui.TableBody as="div">
|
||||
<Row>
|
||||
<fui.Checkbox label={ i18n.t("common.characters.uppercase") } { ...checkboxControls("enableUppercase") } />
|
||||
<fui.Input { ...minInputControls("enableUppercase", "uppercaseCount") } />
|
||||
</Row>
|
||||
<Row>
|
||||
<fui.Checkbox label={ i18n.t("common.characters.lowercase") } { ...checkboxControls("enableLowercase") } />
|
||||
<fui.Input { ...minInputControls("enableLowercase", "lowercaseCount") } />
|
||||
</Row>
|
||||
<Row>
|
||||
<fui.Checkbox label={ i18n.t("common.characters.numeric") } { ...checkboxControls("enableNumeric") } />
|
||||
<fui.Input { ...minInputControls("enableNumeric", "numericCount") } />
|
||||
</Row>
|
||||
<Row>
|
||||
<fui.Checkbox label={ infoLabel(i18n.t("common.characters.special"), CharacterHints.special) } { ...checkboxControls("enableSpecial") } />
|
||||
<fui.Input { ...minInputControls("enableSpecial", "specialCount") } />
|
||||
</Row>
|
||||
<Row>
|
||||
<>
|
||||
<fui.Checkbox { ...checkboxControls("enableCustom") } />
|
||||
<fui.Input size="small" disabled={ !state.enableCustom }
|
||||
placeholder={ i18n.t("common.characters.custom") }
|
||||
value={ state.customSet } onChange={ (_, e) => setState({ customSet: e.value }) } />
|
||||
</>
|
||||
<fui.Input { ...minInputControls("enableCustom", "customCount") } />
|
||||
</Row>
|
||||
</fui.TableBody>
|
||||
</fui.Table>
|
||||
|
||||
<section className={ cls.section }>
|
||||
<fui.Text>{ i18n.t("common.sections.exclude") }</fui.Text>
|
||||
<fui.Checkbox label={ infoLabel(i18n.t("common.characters.similar"), CharacterHints.similar) } { ...checkboxControls("excludeSimilar") } />
|
||||
<fui.Checkbox label={ infoLabel(i18n.t("common.characters.ambiguous"), CharacterHints.ambiguous) } disabled={ !state.enableSpecial } { ...checkboxControls("excludeAmbiguous") } />
|
||||
<fui.Checkbox label={ infoLabel(i18n.t("common.characters.repeating.label"), i18n.t("common.characters.repeating.hint")) } { ...checkboxControls("excludeRepeating") } />
|
||||
<div>
|
||||
<fui.Checkbox { ...checkboxControls("excludeCustom") } />
|
||||
<fui.Input size="small" disabled={ !state.excludeCustom }
|
||||
placeholder={ i18n.t("common.characters.custom") }
|
||||
value={ state.excludeCustomSet } onChange={ (_, e) => setState({ excludeCustomSet: e.value }) } />
|
||||
</div>
|
||||
</section>
|
||||
</GeneratorForm>
|
||||
);
|
||||
}
|
||||
|
||||
function parseCount(value: string): number | null
|
||||
{
|
||||
const n = parseInt(value);
|
||||
return isNaN(n) || n < 1 ? null : Math.min(n, 100);
|
||||
};
|
||||
|
||||
function Row(props: { children: ReactElement[]; }): ReactElement
|
||||
{
|
||||
return (
|
||||
<fui.TableRow as="div">
|
||||
{ props.children.map((i, index) =>
|
||||
<fui.TableCell key={ index } as="div">
|
||||
{ i }
|
||||
</fui.TableCell>
|
||||
) }
|
||||
</fui.TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = fui.makeStyles({
|
||||
section:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
});
|
||||
|
||||
type PasswordSectionState =
|
||||
{
|
||||
length: number | null;
|
||||
enableUppercase: boolean;
|
||||
uppercaseCount: number | null;
|
||||
enableLowercase: boolean;
|
||||
lowercaseCount: number | null;
|
||||
enableNumeric: boolean;
|
||||
numericCount: number | null;
|
||||
enableSpecial: boolean;
|
||||
specialCount: number | null;
|
||||
enableCustom: boolean;
|
||||
customCount: number | null;
|
||||
|
||||
excludeSimilar: boolean;
|
||||
excludeAmbiguous: boolean;
|
||||
excludeRepeating: boolean;
|
||||
excludeCustom: boolean;
|
||||
|
||||
excludeCustomSet: string;
|
||||
customSet: string;
|
||||
};
|
||||
Reference in New Issue
Block a user