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:
@@ -101,16 +101,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: chrome
|
name: chrome
|
||||||
|
|
||||||
- name: Get version from package.json
|
- uses: wdzeng/chrome-extension@v1.3.0
|
||||||
id: get_version
|
|
||||||
run: |
|
|
||||||
extname=`ls password-generator-*-chrome.zip`
|
|
||||||
echo "filename=$extname" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- uses: wdzeng/chrome-extension@v1.2.4
|
|
||||||
with:
|
with:
|
||||||
extension-id: jnjobgjobffgmgfnkpkjfjkkfhfikmfl
|
extension-id: jnjobgjobffgmgfnkpkjfjkkfhfikmfl
|
||||||
zip-path: ${{ steps.get_version.outputs.filename }}
|
zip-path: password-generator-*-chrome.zip
|
||||||
client-id: ${{ secrets.CHROME_CLIENT_ID }}
|
client-id: ${{ secrets.CHROME_CLIENT_ID }}
|
||||||
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
|
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||||
refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 Eugene Fox
|
Copyright (c) 2025 Eugene Fox
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -9,15 +9,14 @@
|
|||||||
<img alt="Password generator">
|
<img alt="Password generator">
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
Extension for web browsers which helps you to easily generate strong passwords in one click
|
Extension for web browsers which helps you to easily generate strong and customizable passwords in a few clicks
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Customizable generator
|
- Customizable generator
|
||||||
- Clean and simple UI
|
- Clean and simple UI
|
||||||
- Dark mode
|
- Dark mode
|
||||||
- **NEW:** Insert and copy generated password in one click
|
- **NEW:** Advanced password generator
|
||||||
|
- **NEW:** Passphrase generator
|
||||||

|
|
||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
- Chinese (Simplified)
|
- Chinese (Simplified)
|
||||||
@@ -93,4 +92,4 @@ If you are interested in fixing issues and contributing directly to the code bas
|
|||||||
[](https://github.com/xfox111)
|
[](https://github.com/xfox111)
|
||||||
[](https://buymeacoffee.com/xfox111)
|
[](https://buymeacoffee.com/xfox111)
|
||||||
|
|
||||||
> ©2024 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/PasswordGeneratorExtension/blob/main/LICENSE)
|
> ©2025 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/PasswordGeneratorExtension/blob/main/LICENSE)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
export default defineBackground(() => main());
|
|
||||||
|
|
||||||
async function main(): Promise<void>
|
|
||||||
{
|
|
||||||
await browser.contextMenus.removeAll();
|
|
||||||
browser.contextMenus.onClicked.addListener(() => browser.action.openPopup());
|
|
||||||
|
|
||||||
const showMenu: boolean = (await storage.getItem<boolean>("sync:ContextMenu", { fallback: true }))!;
|
|
||||||
updateMenus(showMenu);
|
|
||||||
|
|
||||||
storage.watch<boolean>("sync:ContextMenu", e => updateMenus(e!));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateMenus(showMenus: boolean): Promise<void>
|
|
||||||
{
|
|
||||||
await browser.contextMenus.removeAll();
|
|
||||||
|
|
||||||
if (showMenus)
|
|
||||||
browser.contextMenus.create({
|
|
||||||
id: "password-generator",
|
|
||||||
title: i18n.t("manifest.name"),
|
|
||||||
contexts: ["all"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
export default defineContentScript({
|
|
||||||
matches: ["<all_urls>"],
|
|
||||||
runAt: "document_idle",
|
|
||||||
main()
|
|
||||||
{
|
|
||||||
console.log("Password Generator: script loaded");
|
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message: string, _, sendResponse) =>
|
|
||||||
{
|
|
||||||
if (message === "probe")
|
|
||||||
// @ts-expect-error sendResponse has incorrect signature
|
|
||||||
sendResponse(document.querySelectorAll("form input[type=password]").length);
|
|
||||||
else
|
|
||||||
document
|
|
||||||
.querySelectorAll("form input[type=password]")
|
|
||||||
.forEach(el => {
|
|
||||||
(el as HTMLInputElement).value = message;
|
|
||||||
(el as HTMLInputElement).focus();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
export const useStyles = makeStyles({
|
||||||
|
root:
|
||||||
|
{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: tokens.spacingVerticalM,
|
||||||
|
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
|
||||||
|
},
|
||||||
|
horizontalContainer:
|
||||||
|
{
|
||||||
|
display: "flex",
|
||||||
|
gap: tokens.spacingHorizontalSNudge,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { bmcDarkTheme, bmcLightTheme } from "@/utils/BmcTheme";
|
||||||
|
import { getFeedbackLink, githubLinks, personalLinks } from "@/utils/links";
|
||||||
|
import { useTheme } from "@/utils/useTheme";
|
||||||
|
import * as fui from "@fluentui/react-components";
|
||||||
|
import { PersonFeedbackRegular } from "@fluentui/react-icons";
|
||||||
|
import { ReactElement, ReactNode } from "react";
|
||||||
|
import { useStyles } from "./AboutSection.styles";
|
||||||
|
|
||||||
|
export default function AboutSection(): ReactElement
|
||||||
|
{
|
||||||
|
const bmcTheme = useTheme(bmcLightTheme, bmcDarkTheme);
|
||||||
|
const cls = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={ cls.root }>
|
||||||
|
<header className={ cls.horizontalContainer }>
|
||||||
|
<fui.Subtitle1 as="h1">{ i18n.t("manifest.name") }</fui.Subtitle1>
|
||||||
|
<fui.Caption1 as="span">v{ browser.runtime.getManifest().version }</fui.Caption1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<fui.Text as="p">
|
||||||
|
{ i18n.t("about.developed_by") } ({ link("@xfox111.net", personalLinks.social) })
|
||||||
|
<br />
|
||||||
|
{ i18n.t("about.licensed_under") } { link(i18n.t("about.mit_license"), githubLinks.license) }
|
||||||
|
</fui.Text>
|
||||||
|
|
||||||
|
<fui.Text as="p">
|
||||||
|
{ i18n.t("about.translation_cta.text") }<br />
|
||||||
|
{ link(i18n.t("about.translation_cta.button"), githubLinks.translationGuide) }
|
||||||
|
</fui.Text>
|
||||||
|
|
||||||
|
<fui.Text as="p">
|
||||||
|
{ link(i18n.t("about.links.website"), personalLinks.website) } <br />
|
||||||
|
{ link(i18n.t("about.links.source"), githubLinks.repository) } <br />
|
||||||
|
{ link(i18n.t("about.links.changelog"), githubLinks.changelog) }
|
||||||
|
</fui.Text>
|
||||||
|
|
||||||
|
<div className={ cls.horizontalContainer }>
|
||||||
|
<fui.Button { ...buttonProps(getFeedbackLink(), <PersonFeedbackRegular />) }>
|
||||||
|
{ i18n.t("about.cta.feedback") }
|
||||||
|
</fui.Button>
|
||||||
|
<fui.FluentProvider theme={ bmcTheme }>
|
||||||
|
<fui.Button { ...buttonProps(personalLinks.donation, <img style={ { height: 20 } } src="bmc.svg" />) }>
|
||||||
|
{ i18n.t("about.cta.sponsor") }
|
||||||
|
</fui.Button>
|
||||||
|
</fui.FluentProvider>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const link = (text: string, href: string): ReactNode => (
|
||||||
|
<fui.Link target="_blank" href={ href }>{ text }</fui.Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttonProps = (href: string, icon: JSX.Element): fui.ButtonProps => (
|
||||||
|
{
|
||||||
|
as: "a", target: "_blank", href,
|
||||||
|
appearance: "primary", icon
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
export const useStyles = makeStyles({
|
||||||
|
root:
|
||||||
|
{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: tokens.spacingVerticalMNudge,
|
||||||
|
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalMNudge}`,
|
||||||
|
},
|
||||||
|
checkboxContainer:
|
||||||
|
{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
},
|
||||||
|
rangeContainer:
|
||||||
|
{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr auto 1fr auto",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: tokens.spacingHorizontalS,
|
||||||
|
},
|
||||||
|
rangeInput:
|
||||||
|
{
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
excludeCustom:
|
||||||
|
{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "auto 1fr",
|
||||||
|
gap: tokens.spacingHorizontalS,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
divider:
|
||||||
|
{
|
||||||
|
flexGrow: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { CharacterHints } from "@/utils/generators/generatePassword";
|
||||||
|
import { ExtensionOptions, GeneratorOptions, useStorage } from "@/utils/storage";
|
||||||
|
import * as fui from "@fluentui/react-components";
|
||||||
|
import { ArrowUndoRegular } from "@fluentui/react-icons";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import infoLabel from "../../utils/infoLabel";
|
||||||
|
import { useStyles } from "./SettingsSection.styles";
|
||||||
|
|
||||||
|
export default function SettingsSection(): ReactElement
|
||||||
|
{
|
||||||
|
const { extOptions, generatorOptions, updateStorage } = useStorage();
|
||||||
|
const cls = useStyles();
|
||||||
|
|
||||||
|
const resetRange = useCallback(() =>
|
||||||
|
{
|
||||||
|
updateStorage({
|
||||||
|
MinLength: defaultOptions.extension.MinLength,
|
||||||
|
MaxLength: defaultOptions.extension.MaxLength
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setOption = (option: keyof (GeneratorOptions & ExtensionOptions)) =>
|
||||||
|
(_: unknown, args: fui.CheckboxOnChangeData) =>
|
||||||
|
updateStorage({ [option]: args.checked });
|
||||||
|
|
||||||
|
const updateNumberField = (key: keyof (ExtensionOptions & GeneratorOptions), defaultValue: number) =>
|
||||||
|
(_: unknown, e: fui.InputOnChangeData): void =>
|
||||||
|
{
|
||||||
|
if (e.value.length >= 1)
|
||||||
|
{
|
||||||
|
const value = parseInt(e.value);
|
||||||
|
|
||||||
|
if (!isNaN(value) && value >= 0)
|
||||||
|
updateStorage({ [key]: value });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
updateStorage({ [key]: defaultValue });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={ cls.root }>
|
||||||
|
|
||||||
|
<fui.Field label={ i18n.t("settings.length.title") } hint={ i18n.t("settings.length.hint") }>
|
||||||
|
<fui.Input
|
||||||
|
value={ generatorOptions.Length.toString() }
|
||||||
|
onChange={ updateNumberField("Length", 0) } />
|
||||||
|
</fui.Field>
|
||||||
|
|
||||||
|
<fui.Field label={ i18n.t("settings.quick_range") }>
|
||||||
|
<div className={ cls.rangeContainer }>
|
||||||
|
<fui.Input
|
||||||
|
input={ { className: cls.rangeInput } }
|
||||||
|
value={ extOptions.MinLength.toString() }
|
||||||
|
onChange={ updateNumberField("MinLength", defaultOptions.extension.MinLength) } />
|
||||||
|
|
||||||
|
<fui.Divider />
|
||||||
|
|
||||||
|
<fui.Input
|
||||||
|
input={ { className: cls.rangeInput } }
|
||||||
|
value={ extOptions.MaxLength.toString() }
|
||||||
|
onChange={ updateNumberField("MaxLength", defaultOptions.extension.MaxLength) } />
|
||||||
|
|
||||||
|
<fui.Tooltip relationship="label" content={ i18n.t("common.actions.reset") }>
|
||||||
|
<fui.Button
|
||||||
|
appearance="subtle" icon={ <ArrowUndoRegular /> }
|
||||||
|
onClick={ resetRange } />
|
||||||
|
</fui.Tooltip>
|
||||||
|
</div>
|
||||||
|
</fui.Field>
|
||||||
|
|
||||||
|
<fui.Divider className={ cls.divider } />
|
||||||
|
|
||||||
|
<fui.Text>{ i18n.t("common.sections.include") }</fui.Text>
|
||||||
|
<div className={ cls.checkboxContainer }>
|
||||||
|
<fui.Checkbox label={ i18n.t("common.characters.uppercase") }
|
||||||
|
checked={ generatorOptions.Uppercase }
|
||||||
|
onChange={ setOption("Uppercase") } />
|
||||||
|
<fui.Checkbox
|
||||||
|
label={ i18n.t("common.characters.lowercase") }
|
||||||
|
checked={ generatorOptions.Lowercase }
|
||||||
|
onChange={ setOption("Lowercase") } />
|
||||||
|
<fui.Checkbox
|
||||||
|
label={ i18n.t("common.characters.numeric") }
|
||||||
|
checked={ generatorOptions.Numeric }
|
||||||
|
onChange={ setOption("Numeric") } />
|
||||||
|
<fui.Checkbox
|
||||||
|
label={ infoLabel(i18n.t("common.characters.special"), CharacterHints.special) }
|
||||||
|
checked={ generatorOptions.Special }
|
||||||
|
onChange={ setOption("Special") } />
|
||||||
|
<div>
|
||||||
|
<fui.Checkbox checked={ generatorOptions.Custom } onChange={ setOption("Custom") } />
|
||||||
|
<fui.Input size="small" placeholder={ i18n.t("common.characters.custom") }
|
||||||
|
value={ generatorOptions.IncludeCustomSet }
|
||||||
|
onChange={ (_, e) => updateStorage({ IncludeCustomSet: e.value }) } />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fui.Text>{ i18n.t("common.sections.exclude") }</fui.Text>
|
||||||
|
<div className={ cls.checkboxContainer }>
|
||||||
|
<fui.Checkbox
|
||||||
|
label={ infoLabel(i18n.t("common.characters.similar"), CharacterHints.similar) }
|
||||||
|
checked={ generatorOptions.ExcludeSimilar }
|
||||||
|
onChange={ setOption("ExcludeSimilar") } />
|
||||||
|
<fui.Checkbox
|
||||||
|
label={ infoLabel(i18n.t("common.characters.ambiguous"), CharacterHints.ambiguous) }
|
||||||
|
disabled={ !generatorOptions.Special }
|
||||||
|
checked={ generatorOptions.ExcludeAmbiguous }
|
||||||
|
onChange={ setOption("ExcludeAmbiguous") } />
|
||||||
|
<fui.Checkbox
|
||||||
|
label={ infoLabel(i18n.t("common.characters.repeating.label"), i18n.t("common.characters.repeating.hint")) }
|
||||||
|
checked={ generatorOptions.ExcludeRepeating }
|
||||||
|
onChange={ setOption("ExcludeRepeating") } />
|
||||||
|
<div>
|
||||||
|
<fui.Checkbox checked={ generatorOptions.ExcludeCustom } onChange={ setOption("ExcludeCustom") } />
|
||||||
|
<fui.Input size="small" placeholder={ i18n.t("common.characters.custom") }
|
||||||
|
value={ generatorOptions.ExcludeCustomSet }
|
||||||
|
onChange={ (_, e) => updateStorage({ ExcludeCustomSet: e.value }) } />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOptions =
|
||||||
|
{
|
||||||
|
generator: new GeneratorOptions(),
|
||||||
|
extension: new ExtensionOptions()
|
||||||
|
};
|
||||||
@@ -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>Settings - Password generator</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root > .fui-FluentProvider
|
||||||
|
{
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: 450px;
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
section
|
||||||
|
{
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,35 @@
|
|||||||
|
import { Tab, TabList } from "@fluentui/react-components";
|
||||||
|
import { bundleIcon, FluentIcon, Info20Filled, Info20Regular, Settings20Filled, Settings20Regular } from "@fluentui/react-icons";
|
||||||
|
import { ReactElement, StrictMode } from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "../../shared/App";
|
||||||
|
import AboutSection from "./AboutSection";
|
||||||
|
import "./main.css";
|
||||||
|
import SettingsSection from "./SettingsSection";
|
||||||
|
|
||||||
|
function Options(): ReactElement
|
||||||
|
{
|
||||||
|
const [selection, setSelection] = useState<string>("settings");
|
||||||
|
|
||||||
|
const SettingsIcon: FluentIcon = bundleIcon(Settings20Filled, Settings20Regular);
|
||||||
|
const AboutIcon: FluentIcon = bundleIcon(Info20Filled, Info20Regular);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<TabList selectedValue={ selection } onTabSelect={ (_, e) => setSelection(e.value as string) }>
|
||||||
|
<Tab icon={ <SettingsIcon /> } content={ i18n.t("settings.title") } value="settings" />
|
||||||
|
<Tab icon={ <AboutIcon /> } content={ i18n.t("about.title") } value="about" />
|
||||||
|
</TabList>
|
||||||
|
{ selection === "settings" && <SettingsSection /> }
|
||||||
|
{ selection === "about" && <AboutSection /> }
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App>
|
||||||
|
<Options />
|
||||||
|
</App>
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
|
||||||
|
|
||||||
export const useStyles = makeStyles({
|
|
||||||
root:
|
|
||||||
{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
...shorthands.padding(tokens.spacingVerticalM, tokens.spacingHorizontalS),
|
|
||||||
},
|
|
||||||
spinner:
|
|
||||||
{
|
|
||||||
alignSelf: "center",
|
|
||||||
...shorthands.margin(tokens.spacingVerticalXXXL, 0),
|
|
||||||
},
|
|
||||||
accordionAnimation:
|
|
||||||
{
|
|
||||||
"> .fui-AccordionItem .fui-AccordionHeader__expandIcon > svg":
|
|
||||||
{
|
|
||||||
transitionProperty: "transform",
|
|
||||||
transitionDuration: tokens.durationNormal,
|
|
||||||
transitionTimingFunction: tokens.curveDecelerateMax,
|
|
||||||
},
|
|
||||||
"> .fui-AccordionItem > .fui-AccordionPanel":
|
|
||||||
{
|
|
||||||
animationName: "fadeIn",
|
|
||||||
animationDuration: tokens.durationSlow,
|
|
||||||
animationTimingFunction: tokens.curveDecelerateMin,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { StorageProvider } from "@/utils/storage";
|
|
||||||
import { useTheme } from "@/utils/useTheme";
|
|
||||||
import { Accordion, FluentProvider, Spinner } from "@fluentui/react-components";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useStyles } from "./App.styles";
|
|
||||||
import AboutSection from "./sections/AboutSection";
|
|
||||||
import GeneratorView from "./sections/GeneratorView";
|
|
||||||
import SettingsSection from "./sections/SettingsSection";
|
|
||||||
import Snow from "./specials/Snow";
|
|
||||||
|
|
||||||
const App: React.FC = () =>
|
|
||||||
{
|
|
||||||
const theme = useTheme();
|
|
||||||
const cls = useStyles();
|
|
||||||
const [selection, setSelection] = useState<string[]>([]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FluentProvider theme={ theme }>
|
|
||||||
<main className={ cls.root }>
|
|
||||||
<StorageProvider loader={ <Spinner size="large" className={ cls.spinner } /> }>
|
|
||||||
<GeneratorView collapse={ selection.includes("settings") } />
|
|
||||||
<Accordion
|
|
||||||
openItems={ selection }
|
|
||||||
onToggle={ (_, e) => setSelection(e.openItems as string[]) }
|
|
||||||
collapsible>
|
|
||||||
|
|
||||||
<SettingsSection />
|
|
||||||
<AboutSection />
|
|
||||||
</Accordion>
|
|
||||||
</StorageProvider>
|
|
||||||
|
|
||||||
<Snow />
|
|
||||||
</main>
|
|
||||||
</FluentProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
+6
-7
@@ -1,11 +1,6 @@
|
|||||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
|
||||||
export const useStyles = makeStyles({
|
export const useStyles = makeStyles({
|
||||||
root:
|
|
||||||
{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
},
|
|
||||||
input:
|
input:
|
||||||
{
|
{
|
||||||
fontFamily: tokens.fontFamilyMonospace,
|
fontFamily: tokens.fontFamilyMonospace,
|
||||||
@@ -24,6 +19,10 @@ export const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
msgBar:
|
msgBar:
|
||||||
{
|
{
|
||||||
...shorthands.padding(tokens.spacingVerticalMNudge, tokens.spacingHorizontalM),
|
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
|
||||||
},
|
},
|
||||||
|
msgBarBody:
|
||||||
|
{
|
||||||
|
whiteSpace: "break-spaces",
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { generatePassword } from "@/utils/generators/generatePassword";
|
||||||
|
import { GeneratorOptions } from "@/utils/storage";
|
||||||
|
import useTimeout from "@/utils/useTimeout";
|
||||||
|
import { Button, Input, mergeClasses, MessageBar, MessageBarBody, Tooltip } from "@fluentui/react-components";
|
||||||
|
import { ArrowClockwise20Regular, CheckmarkRegular, Copy20Regular } from "@fluentui/react-icons";
|
||||||
|
import { ReactElement, useEffect, useState } from "react";
|
||||||
|
import { useStyles } from "./GeneratorView.styles";
|
||||||
|
|
||||||
|
export default function GeneratorView({ options }: GeneratorViewProps): ReactElement
|
||||||
|
{
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [refreshTimer, copyTimer] = [useTimeout(310), useTimeout(1000)];
|
||||||
|
const cls = useStyles();
|
||||||
|
|
||||||
|
const refresh = useCallback(() =>
|
||||||
|
{
|
||||||
|
if (!options)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
setPassword(generatePassword({
|
||||||
|
length: options.Length,
|
||||||
|
uppercase: options.Uppercase,
|
||||||
|
lowercase: options.Lowercase,
|
||||||
|
numeric: options.Numeric,
|
||||||
|
special: options.Special,
|
||||||
|
custom: options.Custom,
|
||||||
|
excludeSimilar: options.ExcludeSimilar,
|
||||||
|
excludeAmbiguous: options.ExcludeAmbiguous,
|
||||||
|
excludeRepeating: options.ExcludeRepeating,
|
||||||
|
excludeCustom: options.ExcludeCustom ? options.ExcludeCustomSet : "",
|
||||||
|
customSet: options.IncludeCustomSet
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
catch (e) { setError((e as Error).message); }
|
||||||
|
|
||||||
|
refreshTimer.trigger();
|
||||||
|
}, [options]);
|
||||||
|
|
||||||
|
const copy = useCallback(async () =>
|
||||||
|
{
|
||||||
|
await window.navigator.clipboard.writeText(password);
|
||||||
|
copyTimer.trigger();
|
||||||
|
}, [password]);
|
||||||
|
|
||||||
|
useEffect(refresh, [options]);
|
||||||
|
|
||||||
|
if (error)
|
||||||
|
return (
|
||||||
|
<MessageBar intent="warning" className={ cls.msgBar }>
|
||||||
|
<MessageBarBody className={ cls.msgBarBody }>{ error }</MessageBarBody>
|
||||||
|
</MessageBar>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
className={ cls.input }
|
||||||
|
readOnly value={ password }
|
||||||
|
contentAfter={ <>
|
||||||
|
<Tooltip content={ i18n.t("common.actions.copy") } relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle" onClick={ copy }
|
||||||
|
icon={
|
||||||
|
copyTimer.isActive ?
|
||||||
|
<CheckmarkRegular className={ cls.copyIcon } /> :
|
||||||
|
<Copy20Regular className={ cls.copyIcon } />
|
||||||
|
} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={ i18n.t("popup.refresh") } relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle" onClick={ refresh }
|
||||||
|
icon={
|
||||||
|
<ArrowClockwise20Regular className={ mergeClasses(refreshTimer.isActive && cls.refreshIcon) } />
|
||||||
|
} />
|
||||||
|
</Tooltip>
|
||||||
|
</> } />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GeneratorViewProps =
|
||||||
|
{
|
||||||
|
options: GeneratorOptions | null;
|
||||||
|
};
|
||||||
+3
-3
@@ -1,16 +1,16 @@
|
|||||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
|
||||||
export const useStyles = makeStyles({
|
export const useStyles = makeStyles({
|
||||||
characterOptionsContainer:
|
characterOptionsContainer:
|
||||||
{
|
{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
...shorthands.gap(tokens.spacingHorizontalXS),
|
gap: tokens.spacingHorizontalXS,
|
||||||
},
|
},
|
||||||
options:
|
options:
|
||||||
{
|
{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
...shorthands.padding(tokens.spacingVerticalS, tokens.spacingHorizontalS),
|
gap: tokens.spacingVerticalS,
|
||||||
},
|
},
|
||||||
lengthContainer:
|
lengthContainer:
|
||||||
{
|
{
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { GeneratorOptions, useStorage } from "@/utils/storage";
|
import { GeneratorOptions, useStorage } from "@/utils/storage";
|
||||||
import * as fui from "@fluentui/react-components";
|
import * as fui from "@fluentui/react-components";
|
||||||
import * as ic from "@fluentui/react-icons";
|
import * as ic from "@fluentui/react-icons";
|
||||||
import React from "react";
|
import { ReactElement } from "react";
|
||||||
import { useStyles } from "./QuickOptions.styles";
|
import { useStyles } from "./QuickOptions.styles";
|
||||||
|
|
||||||
const QuickOptions: React.FC<IProps> = ({ onChange }) =>
|
export default function QuickOptions({ onChange }: QuickOptionsProps): ReactElement
|
||||||
{
|
{
|
||||||
const { extOptions, generatorOptions } = useStorage();
|
const { extOptions, generatorOptions } = useStorage();
|
||||||
const [quickOpts, setOptions] = useState<GeneratorOptions>(generatorOptions);
|
const [quickOpts, setOptions] = useState<GeneratorOptions>(generatorOptions);
|
||||||
@@ -15,14 +15,13 @@ const QuickOptions: React.FC<IProps> = ({ onChange }) =>
|
|||||||
|
|
||||||
const onCheckedValueChange = useCallback((_: unknown, e: fui.MenuCheckedValueChangeData): void =>
|
const onCheckedValueChange = useCallback((_: unknown, e: fui.MenuCheckedValueChangeData): void =>
|
||||||
{
|
{
|
||||||
const opts: Partial<Omit<GeneratorOptions, "Length">> = {};
|
const opts: Partial<Omit<GeneratorOptions, "Length" | "IncludeCustomSet" | "ExcludeCustomSet">> = {};
|
||||||
|
|
||||||
let keys = Object.keys(quickOpts).filter(i => i !== "Length") as (keyof Omit<GeneratorOptions, "Length">)[];
|
const keys = Object.keys(quickOpts)
|
||||||
|
.filter(i =>
|
||||||
if (e.name === "include")
|
i !== "Length" && i !== "IncludeCustomSet" && i !== "ExcludeCustomSet" &&
|
||||||
keys = keys.filter(i => !i.startsWith("Exclude"));
|
i.startsWith("Exclude") === (e.name === "exclude")
|
||||||
else
|
) as (keyof Omit<GeneratorOptions, "Length" | "IncludeCustomSet" | "ExcludeCustomSet">)[];
|
||||||
keys = keys.filter(i => i.startsWith("Exclude"));
|
|
||||||
|
|
||||||
for (const key of keys)
|
for (const key of keys)
|
||||||
opts[key] = e.checkedItems.includes(key);
|
opts[key] = e.checkedItems.includes(key);
|
||||||
@@ -38,9 +37,6 @@ const QuickOptions: React.FC<IProps> = ({ onChange }) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ cls.options }>
|
<div className={ cls.options }>
|
||||||
<fui.InfoLabel info={ i18n.t("generator.options.hint") }>
|
|
||||||
{ i18n.t("generator.options.title") }
|
|
||||||
</fui.InfoLabel>
|
|
||||||
|
|
||||||
<div className={ cls.lengthContainer }>
|
<div className={ cls.lengthContainer }>
|
||||||
<fui.Slider
|
<fui.Slider
|
||||||
@@ -51,61 +47,79 @@ const QuickOptions: React.FC<IProps> = ({ onChange }) =>
|
|||||||
|
|
||||||
<div className={ cls.characterOptionsContainer }>
|
<div className={ cls.characterOptionsContainer }>
|
||||||
<fui.Menu
|
<fui.Menu
|
||||||
positioning="after" hasCheckmarks
|
positioning={ { position: "after", align: "center", offset: -48 } }
|
||||||
checkedValues={ { include: checkedOptions } }
|
checkedValues={ { include: checkedOptions } }
|
||||||
onCheckedValueChange={ onCheckedValueChange }>
|
onCheckedValueChange={ onCheckedValueChange }>
|
||||||
|
|
||||||
<fui.MenuTrigger disableButtonEnhancement>
|
<fui.MenuTrigger disableButtonEnhancement>
|
||||||
<fui.MenuButton appearance="subtle" icon={ <IncludeIcon /> }>
|
<fui.MenuButton appearance="subtle" icon={ <IncludeIcon /> }>
|
||||||
{ i18n.t("generator.options.include") }
|
{ i18n.t("popup.include") }
|
||||||
</fui.MenuButton>
|
</fui.MenuButton>
|
||||||
</fui.MenuTrigger>
|
</fui.MenuTrigger>
|
||||||
|
|
||||||
<fui.MenuPopover>
|
<fui.MenuPopover>
|
||||||
<fui.MenuList>
|
<fui.MenuList>
|
||||||
<fui.MenuItemCheckbox name="include" value="Uppercase" icon={ <ic.TextCaseUppercaseRegular /> }>
|
<fui.MenuItemCheckbox name="include" value="Uppercase">
|
||||||
{ i18n.t("settings.include.uppercase") }
|
{ i18n.t("common.characters.uppercase") }
|
||||||
</fui.MenuItemCheckbox>
|
</fui.MenuItemCheckbox>
|
||||||
<fui.MenuItemCheckbox name="include" value="Lowercase" icon={ <ic.TextCaseLowercaseRegular /> }>
|
<fui.MenuItemCheckbox name="include" value="Lowercase">
|
||||||
{ i18n.t("settings.include.lowercase") }
|
{ i18n.t("common.characters.lowercase") }
|
||||||
</fui.MenuItemCheckbox>
|
</fui.MenuItemCheckbox>
|
||||||
<fui.MenuItemCheckbox name="include" value="Numeric" icon={ <ic.NumberSymbolRegular /> }>
|
<fui.MenuItemCheckbox name="include" value="Numeric">
|
||||||
{ i18n.t("settings.include.numeric") }
|
{ i18n.t("common.characters.numeric") }
|
||||||
</fui.MenuItemCheckbox>
|
</fui.MenuItemCheckbox>
|
||||||
<fui.MenuItemCheckbox name="include" value="Special" icon={ <ic.MathSymbolsRegular /> }>
|
<fui.MenuItemCheckbox name="include" value="Special">
|
||||||
{ i18n.t("settings.include.special") }
|
{ i18n.t("common.characters.special") }
|
||||||
</fui.MenuItemCheckbox>
|
</fui.MenuItemCheckbox>
|
||||||
|
<fui.MenuSplitGroup>
|
||||||
|
<fui.MenuItemCheckbox name="include" value="Custom"
|
||||||
|
disabled={ generatorOptions.IncludeCustomSet.length < 1 }>
|
||||||
|
|
||||||
|
{ i18n.t("common.characters.custom") }
|
||||||
|
</fui.MenuItemCheckbox>
|
||||||
|
<fui.MenuItem icon={ <ic.EditRegular /> }
|
||||||
|
onClick={ () => browser.runtime.openOptionsPage() } />
|
||||||
|
</fui.MenuSplitGroup>
|
||||||
</fui.MenuList>
|
</fui.MenuList>
|
||||||
</fui.MenuPopover>
|
</fui.MenuPopover>
|
||||||
</fui.Menu>
|
</fui.Menu>
|
||||||
|
|
||||||
<fui.Menu
|
<fui.Menu
|
||||||
positioning="before"
|
positioning={ { position: "after", align: "center", offset: -64 } }
|
||||||
checkedValues={ { exclude: checkedOptions } }
|
checkedValues={ { exclude: checkedOptions } }
|
||||||
onCheckedValueChange={ onCheckedValueChange }>
|
onCheckedValueChange={ onCheckedValueChange }>
|
||||||
|
|
||||||
<fui.MenuTrigger disableButtonEnhancement>
|
<fui.MenuTrigger disableButtonEnhancement>
|
||||||
<fui.MenuButton appearance="subtle" icon={ <ExcludeIcon /> }>
|
<fui.MenuButton appearance="subtle" icon={ <ExcludeIcon /> }>
|
||||||
{ i18n.t("generator.options.exclude") }
|
{ i18n.t("popup.exclude") }
|
||||||
</fui.MenuButton>
|
</fui.MenuButton>
|
||||||
</fui.MenuTrigger>
|
</fui.MenuTrigger>
|
||||||
|
|
||||||
<fui.MenuPopover>
|
<fui.MenuPopover>
|
||||||
<fui.MenuList>
|
<fui.MenuList>
|
||||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeSimilar">
|
<fui.MenuItemCheckbox name="exclude" value="ExcludeSimilar">
|
||||||
{ i18n.t("settings.exclude.similar") }
|
{ i18n.t("common.characters.similar") }
|
||||||
</fui.MenuItemCheckbox>
|
</fui.MenuItemCheckbox>
|
||||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeAmbiguous" disabled={ !quickOpts.Special }>
|
<fui.MenuItemCheckbox name="exclude" value="ExcludeAmbiguous" disabled={ !quickOpts.Special }>
|
||||||
{ i18n.t("settings.exclude.ambiguous") }
|
{ i18n.t("common.characters.ambiguous") }
|
||||||
</fui.MenuItemCheckbox>
|
</fui.MenuItemCheckbox>
|
||||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeRepeating">
|
<fui.MenuItemCheckbox name="exclude" value="ExcludeRepeating">
|
||||||
{ i18n.t("settings.exclude.repeating.title") }
|
{ i18n.t("common.characters.repeating.label") }
|
||||||
</fui.MenuItemCheckbox>
|
</fui.MenuItemCheckbox>
|
||||||
|
<fui.MenuSplitGroup>
|
||||||
|
<fui.MenuItemCheckbox name="exclude" value="ExcludeCustom"
|
||||||
|
disabled={ generatorOptions.ExcludeCustomSet.length < 1 }>
|
||||||
|
|
||||||
|
{ i18n.t("common.characters.custom") }
|
||||||
|
</fui.MenuItemCheckbox>
|
||||||
|
<fui.MenuItem icon={ <ic.EditRegular /> }
|
||||||
|
onClick={ () => browser.runtime.openOptionsPage() } />
|
||||||
|
</fui.MenuSplitGroup>
|
||||||
</fui.MenuList>
|
</fui.MenuList>
|
||||||
</fui.MenuPopover>
|
</fui.MenuPopover>
|
||||||
</fui.Menu>
|
</fui.Menu>
|
||||||
|
|
||||||
<fui.Tooltip content={ i18n.t("common.reset") } relationship="label">
|
<fui.Tooltip content={ i18n.t("common.actions.reset") } relationship="label">
|
||||||
<fui.Button appearance="subtle" icon={ <ic.ArrowUndoRegular /> } onClick={ () => setOptions(generatorOptions) } />
|
<fui.Button appearance="subtle" icon={ <ic.ArrowUndoRegular /> } onClick={ () => setOptions(generatorOptions) } />
|
||||||
</fui.Tooltip>
|
</fui.Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,9 +127,7 @@ const QuickOptions: React.FC<IProps> = ({ onChange }) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QuickOptions;
|
export type QuickOptionsProps =
|
||||||
|
{
|
||||||
interface IProps
|
onChange: (value: GeneratorOptions) => void;
|
||||||
{
|
};
|
||||||
onChange: (value: GeneratorOptions) => void;
|
|
||||||
}
|
|
||||||
@@ -14,12 +14,6 @@ body
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6,
|
|
||||||
p, ul, ol, li
|
|
||||||
{
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scaleUpIn
|
@keyframes scaleUpIn
|
||||||
{
|
{
|
||||||
from
|
from
|
||||||
@@ -60,18 +54,3 @@ p, ul, ol, li
|
|||||||
filter: opacity(1);
|
filter: opacity(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes snowfall
|
|
||||||
{
|
|
||||||
0%
|
|
||||||
{
|
|
||||||
transform: translate3d(var(--left-start), 0, 0);
|
|
||||||
filter: opacity(.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
100%
|
|
||||||
{
|
|
||||||
transform: translate3d(var(--left-end), 610px, 0);
|
|
||||||
filter: opacity(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,62 @@
|
|||||||
|
import { GeneratorOptions } from "@/utils/storage";
|
||||||
|
import { Button, Divider, makeStyles, tokens, Tooltip } from "@fluentui/react-components";
|
||||||
|
import { bundleIcon, FluentIcon, Open20Filled, Open20Regular, Settings20Filled, Settings20Regular } from "@fluentui/react-icons";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "../../shared/App";
|
||||||
import "./style.css";
|
import GeneratorView from "./GeneratorView";
|
||||||
|
import "./main.css";
|
||||||
|
import QuickOptions from "./QuickOptions";
|
||||||
|
|
||||||
|
function Popup(): React.ReactElement
|
||||||
|
{
|
||||||
|
const [options, setOptions] = useState<GeneratorOptions | null>(null);
|
||||||
|
|
||||||
|
const AdvancedIcon: FluentIcon = bundleIcon(Open20Filled, Open20Regular);
|
||||||
|
const SettingsIcon: FluentIcon = bundleIcon(Settings20Filled, Settings20Regular);
|
||||||
|
const cls = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className={ cls.root }>
|
||||||
|
<GeneratorView options={ options } />
|
||||||
|
<QuickOptions onChange={ setOptions } />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div className={ cls.actionsRoot }>
|
||||||
|
<Button as="a" icon={ <AdvancedIcon /> } href="/advanced.html" target="_blank">
|
||||||
|
{ i18n.t("popup.advanced") }
|
||||||
|
</Button>
|
||||||
|
<Tooltip relationship="label" content={ i18n.t("settings.title") }>
|
||||||
|
<Button
|
||||||
|
appearance="subtle" icon={ <SettingsIcon /> }
|
||||||
|
onClick={ () => browser.runtime.openOptionsPage() } />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App>
|
||||||
|
<Popup />
|
||||||
|
</App>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root:
|
||||||
|
{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalS}`,
|
||||||
|
gap: tokens.spacingVerticalMNudge,
|
||||||
|
},
|
||||||
|
actionsRoot:
|
||||||
|
{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr auto",
|
||||||
|
gap: tokens.spacingHorizontalS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
|
||||||
|
|
||||||
export const useStyles = makeStyles({
|
|
||||||
root:
|
|
||||||
{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
...shorthands.gap(tokens.spacingVerticalM),
|
|
||||||
paddingBottom: tokens.spacingVerticalS,
|
|
||||||
},
|
|
||||||
horizontalContainer:
|
|
||||||
{
|
|
||||||
display: "flex",
|
|
||||||
...shorthands.gap(tokens.spacingHorizontalSNudge),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { bmcDarkTheme, bmcLightTheme } from "@/utils/BmcTheme";
|
|
||||||
import { getFeedbackLink, githubLinks, personalLinks } from "@/utils/links";
|
|
||||||
import { useTheme } from "@/utils/useTheme";
|
|
||||||
import * as fui from "@fluentui/react-components";
|
|
||||||
import { InfoRegular, PersonFeedbackRegular } from "@fluentui/react-icons";
|
|
||||||
import { useStyles } from "./AboutSection.styles";
|
|
||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
const link = (text: string, href: string): ReactNode => (
|
|
||||||
<fui.Link target="_blank" href={ href }>{ text }</fui.Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
const buttonProps = (href: string, icon: JSX.Element): fui.ButtonProps => (
|
|
||||||
{
|
|
||||||
as: "a", target: "_blank", href,
|
|
||||||
appearance: "primary", icon
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const AboutSection: React.FC = () =>
|
|
||||||
{
|
|
||||||
const bmcTheme = useTheme(bmcLightTheme, bmcDarkTheme);
|
|
||||||
const cls = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<fui.AccordionItem value="about">
|
|
||||||
<fui.AccordionHeader as="h2" icon={ <InfoRegular /> }>{ i18n.t("about.title") }</fui.AccordionHeader>
|
|
||||||
<fui.AccordionPanel className={ cls.root }>
|
|
||||||
<header className={ cls.horizontalContainer }>
|
|
||||||
<fui.Subtitle1 as="h1">{ i18n.t("manifest.name") }</fui.Subtitle1>
|
|
||||||
<fui.Caption1 as="span">v{ browser.runtime.getManifest().version }</fui.Caption1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<fui.Text as="p">
|
|
||||||
{ i18n.t("about.developed_by") } ({ link("@xfox111", personalLinks.twitter) })
|
|
||||||
<br />
|
|
||||||
{ i18n.t("about.lincensed_under") } { link(i18n.t("about.mit_license"), githubLinks.license) }
|
|
||||||
</fui.Text>
|
|
||||||
|
|
||||||
<fui.Text as="p">
|
|
||||||
{ i18n.t("about.translation_cta.text") }<br />
|
|
||||||
{ link(i18n.t("about.translation_cta.button"), githubLinks.translationGuide) }
|
|
||||||
</fui.Text>
|
|
||||||
|
|
||||||
<fui.Text as="p">
|
|
||||||
{ link(i18n.t("about.links.website"), personalLinks.website) } <br />
|
|
||||||
{ link(i18n.t("about.links.source"), githubLinks.repository) } <br />
|
|
||||||
{ link(i18n.t("about.links.changelog"), githubLinks.changelog) }
|
|
||||||
</fui.Text>
|
|
||||||
|
|
||||||
<div className={ cls.horizontalContainer }>
|
|
||||||
<fui.Button { ...buttonProps(getFeedbackLink(), <PersonFeedbackRegular />) }>
|
|
||||||
{ i18n.t("about.cta.feedback") }
|
|
||||||
</fui.Button>
|
|
||||||
<fui.FluentProvider theme={ bmcTheme }>
|
|
||||||
<fui.Button { ...buttonProps(personalLinks.donation, <img style={ { height: 20 } } src="bmc.svg" />) }>
|
|
||||||
{ i18n.t("about.cta.sponsor") }
|
|
||||||
</fui.Button>
|
|
||||||
</fui.FluentProvider>
|
|
||||||
</div>
|
|
||||||
</fui.AccordionPanel>
|
|
||||||
</fui.AccordionItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AboutSection;
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { GeneratePassword } from "@/utils/PasswordGenerator";
|
|
||||||
import { GeneratorOptions, useStorage } from "@/utils/storage";
|
|
||||||
import useTimeout from "@/utils/useTimeout";
|
|
||||||
import * as fui from "@fluentui/react-components";
|
|
||||||
import * as ic from "@fluentui/react-icons";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useStyles } from "./GeneratorView.styles";
|
|
||||||
import QuickOptions from "./QuickOptions";
|
|
||||||
|
|
||||||
const GeneratorView: React.FC<{ collapse: boolean }> = props =>
|
|
||||||
{
|
|
||||||
const { generatorOptions } = useStorage();
|
|
||||||
const [options, setOptions] = useState<GeneratorOptions>(generatorOptions);
|
|
||||||
const [showInsert, setShowInsert] = useState<boolean>(false);
|
|
||||||
const [password, setPassword] = useState<string>("");
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [refreshTimer, copyTimer, insertTimer] = [useTimeout(310), useTimeout(1000), useTimeout(1000)];
|
|
||||||
const cls = useStyles();
|
|
||||||
|
|
||||||
const refresh = useCallback(() =>
|
|
||||||
{
|
|
||||||
setError(null);
|
|
||||||
try { setPassword(GeneratePassword(options)); }
|
|
||||||
catch (e) { setError((e as Error).message); }
|
|
||||||
}, [options]);
|
|
||||||
|
|
||||||
const copy = useCallback(async () =>
|
|
||||||
{
|
|
||||||
await window.navigator.clipboard.writeText(password);
|
|
||||||
copyTimer.trigger();
|
|
||||||
}, [password]);
|
|
||||||
|
|
||||||
const insert = useCallback(async () =>
|
|
||||||
{
|
|
||||||
const tabId: number = (await browser.tabs.query({ active: true, currentWindow: true }))[0].id!;
|
|
||||||
await browser.tabs.sendMessage(tabId, password);
|
|
||||||
insertTimer.trigger();
|
|
||||||
copy();
|
|
||||||
}, [password]);
|
|
||||||
|
|
||||||
useEffect(() => setOptions(generatorOptions), [generatorOptions]);
|
|
||||||
useEffect(refresh, [options]);
|
|
||||||
useEffect(refreshTimer.trigger, [password]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const tabId: number = (await browser.tabs.query({ active: true, currentWindow: true }))[0].id!;
|
|
||||||
const fieldCount: number = await browser.tabs.sendMessage(tabId, "probe");
|
|
||||||
|
|
||||||
if (fieldCount > 0)
|
|
||||||
setShowInsert(true);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={ cls.root }>
|
|
||||||
{ error ?
|
|
||||||
<fui.MessageBar intent="warning" className={ cls.msgBar }>
|
|
||||||
<fui.MessageBarBody>{ error }</fui.MessageBarBody>
|
|
||||||
</fui.MessageBar>
|
|
||||||
:
|
|
||||||
<fui.Input
|
|
||||||
className={ cls.input }
|
|
||||||
readOnly value={ password }
|
|
||||||
contentAfter={ <>
|
|
||||||
<fui.Tooltip content={ i18n.t("common.copy") } relationship="label">
|
|
||||||
<fui.Button
|
|
||||||
appearance="subtle" onClick={ copy }
|
|
||||||
icon={
|
|
||||||
copyTimer.isActive ?
|
|
||||||
<ic.CheckmarkRegular className={ cls.copyIcon } /> :
|
|
||||||
<ic.CopyRegular className={ cls.copyIcon } />
|
|
||||||
} />
|
|
||||||
</fui.Tooltip>
|
|
||||||
|
|
||||||
<fui.Tooltip content={ i18n.t("generator.refresh") } relationship="label">
|
|
||||||
<fui.Button
|
|
||||||
appearance="subtle" onClick={ refresh }
|
|
||||||
icon={
|
|
||||||
<ic.ArrowClockwiseRegular className={ fui.mergeClasses(refreshTimer.isActive && cls.refreshIcon) } />
|
|
||||||
} />
|
|
||||||
</fui.Tooltip>
|
|
||||||
|
|
||||||
{ showInsert &&
|
|
||||||
<fui.Tooltip content={ i18n.t("generator.insert") } relationship="label">
|
|
||||||
<fui.Button
|
|
||||||
appearance="subtle" onClick={ insert }
|
|
||||||
icon={
|
|
||||||
insertTimer.isActive ?
|
|
||||||
<ic.CheckmarkRegular className={ cls.copyIcon } /> :
|
|
||||||
<ic.ArrowRightRegular className={ cls.copyIcon } />
|
|
||||||
} />
|
|
||||||
</fui.Tooltip>
|
|
||||||
}
|
|
||||||
</> } />
|
|
||||||
}
|
|
||||||
|
|
||||||
{ !props.collapse &&
|
|
||||||
<QuickOptions onChange={ e => setOptions(e) } />
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GeneratorView;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
|
||||||
|
|
||||||
export const useStyles = makeStyles({
|
|
||||||
root:
|
|
||||||
{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
...shorthands.gap(tokens.spacingVerticalS),
|
|
||||||
},
|
|
||||||
checkboxContainer:
|
|
||||||
{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
},
|
|
||||||
rangeContainer:
|
|
||||||
{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr auto 1fr auto",
|
|
||||||
alignItems: "center",
|
|
||||||
...shorthands.gap(tokens.spacingHorizontalS),
|
|
||||||
},
|
|
||||||
rangeInput:
|
|
||||||
{
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import { CharacterHints } from "@/utils/PasswordGenerator";
|
|
||||||
import { ExtensionOptions, GeneratorOptions, useStorage } from "@/utils/storage";
|
|
||||||
import * as fui from "@fluentui/react-components";
|
|
||||||
import { ArrowUndoRegular, SettingsRegular } from "@fluentui/react-icons";
|
|
||||||
import { useStyles } from "./SettingsSection.styles";
|
|
||||||
|
|
||||||
// FIXME: Remove ts-ignore comments once slots override fix is released
|
|
||||||
// Tracker: https://github.com/microsoft/fluentui/issues/27090
|
|
||||||
const infoLabel = (content: string, hint: string) => ({
|
|
||||||
children: (_: unknown, slotProps: fui.LabelProps) => (
|
|
||||||
<fui.InfoLabel { ...slotProps } info={ hint }>{ content }</fui.InfoLabel>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultOptions =
|
|
||||||
{
|
|
||||||
generator: new GeneratorOptions(),
|
|
||||||
extension: new ExtensionOptions()
|
|
||||||
};
|
|
||||||
|
|
||||||
const SettingsSection: React.FC = () =>
|
|
||||||
{
|
|
||||||
const { extOptions, generatorOptions, updateStorage } = useStorage();
|
|
||||||
const cls = useStyles();
|
|
||||||
|
|
||||||
const resetRange = useCallback(() =>
|
|
||||||
{
|
|
||||||
updateStorage({
|
|
||||||
MinLength: defaultOptions.extension.MinLength,
|
|
||||||
MaxLength: defaultOptions.extension.MaxLength
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setOption = (option: keyof (GeneratorOptions & ExtensionOptions)) =>
|
|
||||||
(_: unknown, args: fui.CheckboxOnChangeData) =>
|
|
||||||
updateStorage({ [option]: args.checked });
|
|
||||||
|
|
||||||
const updateNumberField = (key: keyof (ExtensionOptions & GeneratorOptions), defaultValue: number) =>
|
|
||||||
(_: unknown, e: fui.InputOnChangeData): void =>
|
|
||||||
{
|
|
||||||
if (e.value.length >= 1)
|
|
||||||
{
|
|
||||||
const value = parseInt(e.value);
|
|
||||||
|
|
||||||
if (!isNaN(value) && value >= 0)
|
|
||||||
updateStorage({ [key]: value });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
updateStorage({ [key]: defaultValue });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<fui.AccordionItem value="settings">
|
|
||||||
<fui.AccordionHeader as="h2" icon={ <SettingsRegular /> }>{ i18n.t("settings.title") }</fui.AccordionHeader>
|
|
||||||
|
|
||||||
<fui.AccordionPanel className={ cls.root }>
|
|
||||||
|
|
||||||
<fui.Field label={ i18n.t("settings.length.title") } hint={ i18n.t("settings.length.hint") }>
|
|
||||||
<fui.Input
|
|
||||||
value={ generatorOptions.Length.toString() }
|
|
||||||
onChange={ updateNumberField("Length", 0) } />
|
|
||||||
</fui.Field>
|
|
||||||
|
|
||||||
<fui.Field label={ i18n.t("settings.quick_range") }>
|
|
||||||
<div className={ cls.rangeContainer }>
|
|
||||||
<fui.Input
|
|
||||||
input={ { className: cls.rangeInput } }
|
|
||||||
value={ extOptions.MinLength.toString() }
|
|
||||||
onChange={ updateNumberField("MinLength", defaultOptions.extension.MinLength) } />
|
|
||||||
|
|
||||||
<fui.Divider />
|
|
||||||
|
|
||||||
<fui.Input
|
|
||||||
input={ { className: cls.rangeInput } }
|
|
||||||
value={ extOptions.MaxLength.toString() }
|
|
||||||
onChange={ updateNumberField("MaxLength", defaultOptions.extension.MaxLength) } />
|
|
||||||
|
|
||||||
<fui.Tooltip relationship="label" content={ i18n.t("common.reset") }>
|
|
||||||
<fui.Button
|
|
||||||
appearance="subtle" icon={ <ArrowUndoRegular /> }
|
|
||||||
onClick={ resetRange } />
|
|
||||||
</fui.Tooltip>
|
|
||||||
</div>
|
|
||||||
</fui.Field>
|
|
||||||
|
|
||||||
<fui.Divider />
|
|
||||||
|
|
||||||
<fui.Text>{ i18n.t("settings.include.title") }</fui.Text>
|
|
||||||
<div className={ cls.checkboxContainer }>
|
|
||||||
<fui.Checkbox label={ i18n.t("settings.include.uppercase") }
|
|
||||||
checked={ generatorOptions.Uppercase }
|
|
||||||
onChange={ setOption("Uppercase") } />
|
|
||||||
<fui.Checkbox
|
|
||||||
label={ i18n.t("settings.include.lowercase") }
|
|
||||||
checked={ generatorOptions.Lowercase }
|
|
||||||
onChange={ setOption("Lowercase") } />
|
|
||||||
<fui.Checkbox
|
|
||||||
label={ i18n.t("settings.include.numeric") }
|
|
||||||
checked={ generatorOptions.Numeric }
|
|
||||||
onChange={ setOption("Numeric") } />
|
|
||||||
<fui.Checkbox
|
|
||||||
label={ i18n.t("settings.include.special") }
|
|
||||||
checked={ generatorOptions.Special }
|
|
||||||
onChange={ setOption("Special") } />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fui.Text>{ i18n.t("settings.exclude.title") }</fui.Text>
|
|
||||||
<div className={ cls.checkboxContainer }>
|
|
||||||
<fui.Checkbox
|
|
||||||
// @ts-expect-error See FIXME
|
|
||||||
label={ infoLabel(i18n.t("settings.exclude.similar"), CharacterHints.Similar) }
|
|
||||||
checked={ generatorOptions.ExcludeSimilar }
|
|
||||||
onChange={ setOption("ExcludeSimilar") } />
|
|
||||||
<fui.Checkbox
|
|
||||||
// @ts-expect-error See FIXME
|
|
||||||
label={ infoLabel(i18n.t("settings.exclude.ambiguous"), CharacterHints.Ambiguous) }
|
|
||||||
disabled={ !generatorOptions.Special }
|
|
||||||
checked={ generatorOptions.ExcludeAmbiguous }
|
|
||||||
onChange={ setOption("ExcludeAmbiguous") } />
|
|
||||||
<fui.Checkbox
|
|
||||||
// @ts-expect-error See FIXME
|
|
||||||
label={ infoLabel(i18n.t("settings.exclude.repeating.title"), i18n.t("settings.exclude.repeating.hint")) }
|
|
||||||
checked={ generatorOptions.ExcludeRepeating }
|
|
||||||
onChange={ setOption("ExcludeRepeating") } />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fui.Checkbox
|
|
||||||
label={ i18n.t("settings.context_menu") }
|
|
||||||
checked={ extOptions.ContextMenu }
|
|
||||||
onChange={ setOption("ContextMenu") } />
|
|
||||||
|
|
||||||
</fui.AccordionPanel>
|
|
||||||
|
|
||||||
</fui.AccordionItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SettingsSection;
|
|
||||||
+1
-1
@@ -44,7 +44,7 @@ export default [
|
|||||||
"indent": ["warn", "tab", { "SwitchCase": 1 }],
|
"indent": ["warn", "tab", { "SwitchCase": 1 }],
|
||||||
"no-empty": "off",
|
"no-empty": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"@typescript-eslint/no-unused-vars": ["error", {
|
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||||
"argsIgnorePattern": "^_",
|
"argsIgnorePattern": "^_",
|
||||||
"args": "none"
|
"args": "none"
|
||||||
}],
|
}],
|
||||||
|
|||||||
+49
-26
@@ -1,11 +1,26 @@
|
|||||||
manifest:
|
manifest:
|
||||||
name: "Password generator"
|
name: "Password generator"
|
||||||
description: "Password generator extension allows you to easily generate long and secure password in one click"
|
description: "Extension which helps you to easily generate strong and customizable passwords in a few clicks"
|
||||||
author: "Eugene Fox"
|
author: "Eugene Fox"
|
||||||
|
|
||||||
common:
|
common:
|
||||||
reset: "Reset"
|
actions:
|
||||||
copy: "Copy"
|
reset: "Reset"
|
||||||
|
copy: "Copy"
|
||||||
|
characters:
|
||||||
|
uppercase: "Uppercase"
|
||||||
|
lowercase: "Lowercase"
|
||||||
|
numeric: "Numeric"
|
||||||
|
special: "Special"
|
||||||
|
similar: "Similar"
|
||||||
|
ambiguous: "Ambiguous"
|
||||||
|
repeating:
|
||||||
|
label: "Repeating"
|
||||||
|
hint: "Each character in the password will be unique"
|
||||||
|
custom: "Custom"
|
||||||
|
sections:
|
||||||
|
include: "Include symbols"
|
||||||
|
exclude: "Exclude symbols"
|
||||||
|
|
||||||
errors:
|
errors:
|
||||||
too_short: "Password length must be at least 4 characters"
|
too_short: "Password length must be at least 4 characters"
|
||||||
@@ -15,7 +30,7 @@ errors:
|
|||||||
about:
|
about:
|
||||||
title: "About"
|
title: "About"
|
||||||
developed_by: "Developed by Eugene Fox"
|
developed_by: "Developed by Eugene Fox"
|
||||||
lincensed_under: "Licensed under"
|
licensed_under: "Licensed under"
|
||||||
mit_license: "MIT License"
|
mit_license: "MIT License"
|
||||||
translation_cta:
|
translation_cta:
|
||||||
text: "Found a typo or want a translation for your language?"
|
text: "Found a typo or want a translation for your language?"
|
||||||
@@ -28,32 +43,40 @@ about:
|
|||||||
feedback: "Leave feedback"
|
feedback: "Leave feedback"
|
||||||
sponsor: "Buy me a coffee"
|
sponsor: "Buy me a coffee"
|
||||||
|
|
||||||
|
popup:
|
||||||
|
refresh: "Generate new"
|
||||||
|
include: "Include"
|
||||||
|
exclude: "Exclude"
|
||||||
|
advanced: "Advanced"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
title: "Settings"
|
title: "Settings"
|
||||||
length:
|
length:
|
||||||
title: "Default password length"
|
title: "Default password length"
|
||||||
hint: "Recommended length: 8–16"
|
hint: "Recommended length: 8–16"
|
||||||
quick_range: "Quick adjustment length range"
|
quick_range: "Quick adjustment length range"
|
||||||
include:
|
exclude_custom: "Do not use these characters:"
|
||||||
title: "Include symbols"
|
|
||||||
uppercase: "Uppercase"
|
|
||||||
lowercase: "Lowercase"
|
|
||||||
numeric: "Numeric"
|
|
||||||
special: "Special"
|
|
||||||
exclude:
|
|
||||||
title: "Exclude symbols"
|
|
||||||
similar: "Similar"
|
|
||||||
ambiguous: "Ambiguous"
|
|
||||||
repeating:
|
|
||||||
title: "Repeating"
|
|
||||||
hint: "Each character in the password will be unique"
|
|
||||||
context_menu: "Show context menu"
|
|
||||||
|
|
||||||
generator:
|
advanced:
|
||||||
refresh: "Refresh"
|
title: "Advanced password generator"
|
||||||
insert: "Insert and copy"
|
saved_msg: "Configuration saved"
|
||||||
options:
|
copied_msg: "Copied to clipboard"
|
||||||
title: "Quick adjustments"
|
actions:
|
||||||
hint: "These adjustments will not be saved"
|
generate: "Generate"
|
||||||
include: "Include"
|
save_config: "Save configuration"
|
||||||
exclude: "Exclude"
|
copy_all: "Copy all"
|
||||||
|
password:
|
||||||
|
title: "Password generator"
|
||||||
|
length: "Password length"
|
||||||
|
min_of_type: "Minimum number of characters"
|
||||||
|
passphrase:
|
||||||
|
title: "Passphrase generator"
|
||||||
|
length: "Number of words"
|
||||||
|
replace: "Replace random characters"
|
||||||
|
random_case: "Randomize character case"
|
||||||
|
allow_repat:
|
||||||
|
label: "Allow repeating"
|
||||||
|
hint: "If disabled, each word in the passphrase will be unique"
|
||||||
|
separate_words:
|
||||||
|
label: "Separate words with"
|
||||||
|
hint: "If enabled, provided sequence will be placed between each word. If the field is empty - whitespace will be used"
|
||||||
|
|||||||
+48
-25
@@ -1,11 +1,26 @@
|
|||||||
manifest:
|
manifest:
|
||||||
name: "Generator haseł"
|
name: "Generator haseł"
|
||||||
description: "Rozszerzenie, które pozwala na łatwe generowanie trudnych i bezpiecznych haseł w jednym kliknięciu"
|
description: "Rozszerzenie, które pomaga w łatwy sposób generować silne i dostosowane hasła w kilku kliknięciach"
|
||||||
author: "Jewgienij Lis"
|
author: "Jewgienij Lis"
|
||||||
|
|
||||||
common:
|
common:
|
||||||
reset: "Resetuj"
|
actions:
|
||||||
copy: "Kopiuj"
|
reset: "Resetuj"
|
||||||
|
copy: "Kopiuj"
|
||||||
|
characters:
|
||||||
|
uppercase: "Wielkie"
|
||||||
|
lowercase: "Małe"
|
||||||
|
numeric: "Cyfry"
|
||||||
|
special: "Specjalne"
|
||||||
|
similar: "Podobne"
|
||||||
|
ambiguous: "Niebezpieczne"
|
||||||
|
repeating:
|
||||||
|
label: "Powtarzające się"
|
||||||
|
hint: "Każdy znak hasła będzie unikalny"
|
||||||
|
custom: "Niestandardowe"
|
||||||
|
sections:
|
||||||
|
include: "Dodaj znaki"
|
||||||
|
exclude: "Wyklucz znaki"
|
||||||
|
|
||||||
errors:
|
errors:
|
||||||
too_short: "Minimalna długość hasła to 4 znaki"
|
too_short: "Minimalna długość hasła to 4 znaki"
|
||||||
@@ -28,32 +43,40 @@ about:
|
|||||||
feedback: "Zostaw opinię"
|
feedback: "Zostaw opinię"
|
||||||
sponsor: "Wesprzyj"
|
sponsor: "Wesprzyj"
|
||||||
|
|
||||||
|
popup:
|
||||||
|
refresh: "Wygeneruj nowe"
|
||||||
|
include: "Dodaj"
|
||||||
|
exclude: "Wyklucz"
|
||||||
|
advanced: "Zaawansowane"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
title: "Ustawienia"
|
title: "Ustawienia"
|
||||||
length:
|
length:
|
||||||
title: "Domyślna długość hasła"
|
title: "Domyślna długość hasła"
|
||||||
hint: "Zalecana długość: 8–16"
|
hint: "Zalecana długość: 8–16"
|
||||||
quick_range: "Zakres długości dla szybkich ustawień"
|
quick_range: "Zakres długości dla szybkich ustawień"
|
||||||
include:
|
exclude_custom: "Nie używaj tych znaków:"
|
||||||
title: "Dodaj znaki"
|
|
||||||
uppercase: "Wielkie"
|
|
||||||
lowercase: "Małe"
|
|
||||||
numeric: "Cyfry"
|
|
||||||
special: "Specjalne"
|
|
||||||
exclude:
|
|
||||||
title: "Wyklucz znaki"
|
|
||||||
similar: "Podobne"
|
|
||||||
ambiguous: "Niebezpieczne"
|
|
||||||
repeating:
|
|
||||||
title: "Powtarzające się"
|
|
||||||
hint: "Każdy znak hasła będzie unikalny"
|
|
||||||
context_menu: "Pokaż menu kontekstowe"
|
|
||||||
|
|
||||||
generator:
|
advanced:
|
||||||
refresh: "Wygeneruj nowe"
|
title: "Zaawansowany generator haseł"
|
||||||
insert: "Wstaw i kopiuj"
|
saved_msg: "Konfiguracja zapisana"
|
||||||
options:
|
copied_msg: "Skopiowano do schowka"
|
||||||
title: "Szybkie ustawienia"
|
actions:
|
||||||
hint: "Te ustawienia nie zostaną zapisane"
|
generate: "Wygeneruj"
|
||||||
include: "Dodaj"
|
save_config: "Zapisz konfigurację"
|
||||||
exclude: "Wyklucz"
|
copy_all: "Kopiuj wszystko"
|
||||||
|
password:
|
||||||
|
title: "Generator haseł"
|
||||||
|
length: "Długość hasła"
|
||||||
|
min_of_type: "Minimum"
|
||||||
|
passphrase:
|
||||||
|
title: "Frazy hasłowe"
|
||||||
|
length: "Liczba słów"
|
||||||
|
replace: "Zastąp losowe znaki"
|
||||||
|
random_case: "Losowa wielkość liter"
|
||||||
|
allow_repat:
|
||||||
|
label: "Zezwól na powtórzenia"
|
||||||
|
hint: "Jeśli wyłączone, każde słowo w frazie będzie unikalne"
|
||||||
|
separate_words:
|
||||||
|
label: "Oddziel słowa"
|
||||||
|
hint: "Jeśli włączone, podana sekwencja zostanie wstawiona między każde słowo. Jeśli pole jest puste, użyta zostanie spacja"
|
||||||
|
|||||||
+48
-25
@@ -1,11 +1,26 @@
|
|||||||
manifest:
|
manifest:
|
||||||
name: "Gerador de Senhas"
|
name: "Gerador de Senhas"
|
||||||
description: "A extensão do gerador de senhas permite gerar facilmente uma senha longa e segura em um clique"
|
description: "Extensão que ajuda você a gerar senhas fortes e personalizadas facilmente em poucos cliques"
|
||||||
author: "Eugênio Fox"
|
author: "Eugênio Fox"
|
||||||
|
|
||||||
common:
|
common:
|
||||||
reset: "Redefinir"
|
actions:
|
||||||
copy: "Copiar"
|
reset: "Redefinir"
|
||||||
|
copy: "Copiar"
|
||||||
|
characters:
|
||||||
|
uppercase: "Maiúsculos"
|
||||||
|
lowercase: "Minúsculos"
|
||||||
|
numeric: "Numéricos"
|
||||||
|
special: "Especiais"
|
||||||
|
similar: "Semelhantes"
|
||||||
|
ambiguous: "Ambíguos"
|
||||||
|
repeating:
|
||||||
|
label: "Repetidos"
|
||||||
|
hint: "Cada caractere na senha será único"
|
||||||
|
custom: "Personalizado"
|
||||||
|
sections:
|
||||||
|
include: "Incluir símbolos"
|
||||||
|
exclude: "Excluir símbolos"
|
||||||
|
|
||||||
errors:
|
errors:
|
||||||
too_short: "O comprimento da senha deve ter pelo menos 4 caracteres"
|
too_short: "O comprimento da senha deve ter pelo menos 4 caracteres"
|
||||||
@@ -28,32 +43,40 @@ about:
|
|||||||
feedback: "Enviar comentários"
|
feedback: "Enviar comentários"
|
||||||
sponsor: "Pague um café"
|
sponsor: "Pague um café"
|
||||||
|
|
||||||
|
popup:
|
||||||
|
refresh: "Gerar nova"
|
||||||
|
include: "Incluir"
|
||||||
|
exclude: "Excluir"
|
||||||
|
advanced: "Avançado"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
title: "Configurações"
|
title: "Configurações"
|
||||||
length:
|
length:
|
||||||
title: "Comprimento padrão da senha"
|
title: "Comprimento padrão da senha"
|
||||||
hint: "Comprimento recomendado: 8–16"
|
hint: "Comprimento recomendado: 8–16"
|
||||||
quick_range: "Intervalo do comprimento de ajuste rápido"
|
quick_range: "Intervalo do comprimento de ajuste rápido"
|
||||||
include:
|
exclude_custom: "Não use esses caracteres:"
|
||||||
title: "Incluir símbolos"
|
|
||||||
uppercase: "Maiúsculos"
|
|
||||||
lowercase: "Minúsculos"
|
|
||||||
numeric: "Numéricos"
|
|
||||||
special: "Especiais"
|
|
||||||
exclude:
|
|
||||||
title: "Excluir símbolos"
|
|
||||||
similar: "Semelhantes"
|
|
||||||
ambiguous: "Ambíguos"
|
|
||||||
repeating:
|
|
||||||
title: "Repetidos"
|
|
||||||
hint: "Cada caractere na senha será único"
|
|
||||||
context_menu: "Mostrar menu de contexto"
|
|
||||||
|
|
||||||
generator:
|
advanced:
|
||||||
refresh: "Gerar nova"
|
title: "Gerador de senha avançado"
|
||||||
insert: "Inserir e copiar"
|
saved_msg: "Configuração salva"
|
||||||
options:
|
copied_msg: "Copiado para a área de transferência"
|
||||||
title: "Ajustes rápidos"
|
actions:
|
||||||
hint: "Esses ajustes não serão salvos"
|
generate: "Gerar"
|
||||||
include: "Incluir"
|
save_config: "Salvar configuração"
|
||||||
exclude: "Excluir"
|
copy_all: "Copiar tudo"
|
||||||
|
password:
|
||||||
|
title: "Gerador de senhas"
|
||||||
|
length: "Comprimento da senha"
|
||||||
|
min_of_type: "Número mínimo de caracteres"
|
||||||
|
passphrase:
|
||||||
|
title: "Gerador de frases-senha"
|
||||||
|
length: "Número de palavras"
|
||||||
|
replace: "Substituir caracteres aleatórios"
|
||||||
|
random_case: "Aleatorizar maiúsculas e minúsculas"
|
||||||
|
allow_repat:
|
||||||
|
label: "Permitir repetição"
|
||||||
|
hint: "Se desativado, cada palavra na frase-senha será única"
|
||||||
|
separate_words:
|
||||||
|
label: "Separar palavras com"
|
||||||
|
hint: "Se ativado, a sequência fornecida será colocada entre cada palavra. Se o campo estiver vazio, será usado um espaço em branco"
|
||||||
|
|||||||
+49
-26
@@ -1,11 +1,26 @@
|
|||||||
manifest:
|
manifest:
|
||||||
name: "Генератор паролей"
|
name: "Генератор паролей"
|
||||||
description: "Расширение, позволяющее легко генерировать сложные и надежные пароли в один клик"
|
description: "Расширение, которое поможет вам легко создавать надежные и настраиваемые пароли всего в несколько кликов"
|
||||||
author: "Евгений Лис"
|
author: "Евгений Лис"
|
||||||
|
|
||||||
common:
|
common:
|
||||||
reset: "Сбросить"
|
actions:
|
||||||
copy: "Копировать"
|
reset: "Сбросить"
|
||||||
|
copy: "Копировать"
|
||||||
|
characters:
|
||||||
|
uppercase: "Прописные"
|
||||||
|
lowercase: "Строчные"
|
||||||
|
numeric: "Числовые"
|
||||||
|
special: "Специальные"
|
||||||
|
similar: "Похожие"
|
||||||
|
ambiguous: "Особые"
|
||||||
|
repeating:
|
||||||
|
label: "Повторяющиеся"
|
||||||
|
hint: "Каждый символ пароля будет уникальным"
|
||||||
|
custom: "Пользовательские"
|
||||||
|
sections:
|
||||||
|
include: "Добавить символы"
|
||||||
|
exclude: "Исключить символы"
|
||||||
|
|
||||||
errors:
|
errors:
|
||||||
too_short: "Минимальная длина пароля — 4 символа"
|
too_short: "Минимальная длина пароля — 4 символа"
|
||||||
@@ -14,7 +29,7 @@ errors:
|
|||||||
|
|
||||||
about:
|
about:
|
||||||
title: "О расширении"
|
title: "О расширении"
|
||||||
developed_by: "Разработчк: Евгений Лис"
|
developed_by: "Разработчик: Евгений Лис"
|
||||||
licensed_under: ""
|
licensed_under: ""
|
||||||
mit_license: "Лицензия MIT"
|
mit_license: "Лицензия MIT"
|
||||||
translation_cta:
|
translation_cta:
|
||||||
@@ -28,32 +43,40 @@ about:
|
|||||||
feedback: "Оставить отзыв"
|
feedback: "Оставить отзыв"
|
||||||
sponsor: "Поддержать"
|
sponsor: "Поддержать"
|
||||||
|
|
||||||
|
popup:
|
||||||
|
refresh: "Сгенерировать новый"
|
||||||
|
include: "Добавить"
|
||||||
|
exclude: "Исключить"
|
||||||
|
advanced: "Расширенный"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
title: "Настройки"
|
title: "Настройки"
|
||||||
length:
|
length:
|
||||||
title: "Длина пароля по умолчанию"
|
title: "Длина пароля по умолчанию"
|
||||||
hint: "Рекомендованная длина: 8–16"
|
hint: "Рекомендованная длина: 8–16"
|
||||||
quick_range: "Диапазон длины для быстрых настроек"
|
quick_range: "Диапазон длины для быстрых настроек"
|
||||||
include:
|
exclude_custom: "Не использовать эти символы:"
|
||||||
title: "Добавить символы"
|
|
||||||
uppercase: "Прописные"
|
|
||||||
lowercase: "Строчные"
|
|
||||||
numeric: "Числовые"
|
|
||||||
special: "Специальные"
|
|
||||||
exclude:
|
|
||||||
title: "Исключить символы"
|
|
||||||
similar: "Похожие"
|
|
||||||
ambiguous: "Особые"
|
|
||||||
repeating:
|
|
||||||
title: "Повторяющиеся"
|
|
||||||
hint: "Каждый символ пароля будет уникальным"
|
|
||||||
context_menu: "Показать контекстное меню"
|
|
||||||
|
|
||||||
generator:
|
advanced:
|
||||||
refresh: "Сгенерировать новый"
|
title: "Расширенный генератор паролей"
|
||||||
insert: "Вставить и копировать"
|
saved_msg: "Конфигурация сохранена"
|
||||||
options:
|
copied_msg: "Скопировано в буфер обмена"
|
||||||
title: "Быстрые настройки"
|
actions:
|
||||||
hint: "Эти настройки не будут сохранены"
|
generate: "Сгенерировать"
|
||||||
include: "Добавить"
|
save_config: "Сохранить конфигурацию"
|
||||||
exclude: "Исключить"
|
copy_all: "Копировать все"
|
||||||
|
password:
|
||||||
|
title: "Генератор паролей"
|
||||||
|
length: "Длина пароля"
|
||||||
|
min_of_type: "Не менее"
|
||||||
|
passphrase:
|
||||||
|
title: "Парольных фраз"
|
||||||
|
length: "Количество слов"
|
||||||
|
replace: "Заменить случайные символы"
|
||||||
|
random_case: "Случайный регистр символов"
|
||||||
|
allow_repat:
|
||||||
|
label: "Разрешить повторение"
|
||||||
|
hint: "Если отключено, каждое слово в фразе будет уникальным"
|
||||||
|
separate_words:
|
||||||
|
label: "Разделить слова"
|
||||||
|
hint: "Если включено, указанная последовательность будет вставлена между каждым словом. Если поле пустое - будет использован пробел"
|
||||||
|
|||||||
+48
-25
@@ -1,11 +1,26 @@
|
|||||||
manifest:
|
manifest:
|
||||||
name: "Генератор паролів"
|
name: "Генератор паролів"
|
||||||
description: "Розширення, яке дозволяє легко генерувати складні та надійні паролі в один клік"
|
description: "Розширення, яке допоможе вам легко створювати надійні та налаштовані паролі всього в декілька клацань"
|
||||||
author: "Євген Лис"
|
author: "Євген Лис"
|
||||||
|
|
||||||
common:
|
common:
|
||||||
reset: "Скинути"
|
actions:
|
||||||
copy: "Копіювати"
|
reset: "Скинути"
|
||||||
|
copy: "Копіювати"
|
||||||
|
characters:
|
||||||
|
uppercase: "Великі"
|
||||||
|
lowercase: "Малі"
|
||||||
|
numeric: "Числові"
|
||||||
|
special: "Спеціальні"
|
||||||
|
similar: "Схожі"
|
||||||
|
ambiguous: "Особливі"
|
||||||
|
repeating:
|
||||||
|
label: "Повторювані"
|
||||||
|
hint: "Кожен символ пароля буде унікальним"
|
||||||
|
custom: "Користувацькі"
|
||||||
|
sections:
|
||||||
|
include: "Додати символи"
|
||||||
|
exclude: "Виключити символи"
|
||||||
|
|
||||||
errors:
|
errors:
|
||||||
too_short: "Мінімальна довжина пароля — 4 символи"
|
too_short: "Мінімальна довжина пароля — 4 символи"
|
||||||
@@ -28,32 +43,40 @@ about:
|
|||||||
feedback: "Залишити відгук"
|
feedback: "Залишити відгук"
|
||||||
sponsor: "Підтримати"
|
sponsor: "Підтримати"
|
||||||
|
|
||||||
|
popup:
|
||||||
|
refresh: "Згенерувати новий"
|
||||||
|
include: "Додати"
|
||||||
|
exclude: "Виключити"
|
||||||
|
advanced: "Розширений"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
title: "Налаштування"
|
title: "Налаштування"
|
||||||
length:
|
length:
|
||||||
title: "Довжина пароля за замовчуванням"
|
title: "Довжина пароля за замовчуванням"
|
||||||
hint: "Рекомендована довжина: 8–16"
|
hint: "Рекомендована довжина: 8–16"
|
||||||
quick_range: "Діапазон довжини для швидких налаштувань"
|
quick_range: "Діапазон довжини для швидких налаштувань"
|
||||||
include:
|
exclude_custom: "Не використовувати ці символи:"
|
||||||
title: "Додати символи"
|
|
||||||
uppercase: "Великі"
|
|
||||||
lowercase: "Малі"
|
|
||||||
numeric: "Числові"
|
|
||||||
special: "Спеціальні"
|
|
||||||
exclude:
|
|
||||||
title: "Виключити символи"
|
|
||||||
similar: "Схожі"
|
|
||||||
ambiguous: "Особливі"
|
|
||||||
repeating:
|
|
||||||
title: "Повторювані"
|
|
||||||
hint: "Кожен символ пароля буде унікальним"
|
|
||||||
context_menu: "Показати контекстне меню"
|
|
||||||
|
|
||||||
generator:
|
advanced:
|
||||||
refresh: "Згенерувати новий"
|
title: "Розширений генератор паролів"
|
||||||
insert: "Вставити та копіювати"
|
saved_msg: "Конфігурація збережена"
|
||||||
options:
|
copied_msg: "Скопійовано у буфер обміну"
|
||||||
title: "Швидкі налаштування"
|
actions:
|
||||||
hint: "Ці налаштування не будуть збережені"
|
generate: "Згенерувати"
|
||||||
include: "Додати"
|
save_config: "Зберегти конфігурацію"
|
||||||
exclude: "Виключити"
|
copy_all: "Копіювати все"
|
||||||
|
password:
|
||||||
|
title: "Генератор паролів"
|
||||||
|
length: "Довжина пароля"
|
||||||
|
min_of_type: "Не менше"
|
||||||
|
passphrase:
|
||||||
|
title: "Парольних фраз"
|
||||||
|
length: "Кількість слів"
|
||||||
|
replace: "Замінити випадкові символи"
|
||||||
|
random_case: "Випадковий регістр символів"
|
||||||
|
allow_repat:
|
||||||
|
label: "Дозволити повторення"
|
||||||
|
hint: "Якщо вимкнено, кожне слово у фразі буде унікальним"
|
||||||
|
separate_words:
|
||||||
|
label: "Розділити слова"
|
||||||
|
hint: "Якщо увімкнено, вказана послідовність буде вставлена між кожним словом. Якщо поле порожнє - буде використано пробіл"
|
||||||
|
|||||||
+49
-26
@@ -1,11 +1,26 @@
|
|||||||
manifest:
|
manifest:
|
||||||
name: "密码生成器"
|
name: "密码生成器"
|
||||||
description: "密码生成器扩展可让您一键轻松生成长而安全的密码"
|
description: "这个扩展可以帮助您轻松生成强大且可定制的密码"
|
||||||
author: "Eugene Fox"
|
author: "Eugene Fox"
|
||||||
|
|
||||||
common:
|
common:
|
||||||
reset: "重置"
|
actions:
|
||||||
copy: "复制"
|
reset: "重置"
|
||||||
|
copy: "复制"
|
||||||
|
characters:
|
||||||
|
uppercase: "大写字母"
|
||||||
|
lowercase: "小写字母"
|
||||||
|
numeric: "数字"
|
||||||
|
special: "特殊"
|
||||||
|
similar: "相似"
|
||||||
|
ambiguous: "易混淆"
|
||||||
|
repeating:
|
||||||
|
label: "重复"
|
||||||
|
hint: "密码中的每个字符都将是独一无二的"
|
||||||
|
custom: "自定义"
|
||||||
|
sections:
|
||||||
|
include: "包括字符"
|
||||||
|
exclude: "排除字符"
|
||||||
|
|
||||||
errors:
|
errors:
|
||||||
too_short: "密码的长度必须至少为4个字符"
|
too_short: "密码的长度必须至少为4个字符"
|
||||||
@@ -15,7 +30,7 @@ errors:
|
|||||||
about:
|
about:
|
||||||
title: "关于"
|
title: "关于"
|
||||||
developed_by: "由 Eugene Fox 开发"
|
developed_by: "由 Eugene Fox 开发"
|
||||||
lincensed_under: "协议:"
|
licensed_under: "协议:"
|
||||||
mit_license: "MIT License"
|
mit_license: "MIT License"
|
||||||
translation_cta:
|
translation_cta:
|
||||||
text: "发现错别字或需要翻译成您的语言?"
|
text: "发现错别字或需要翻译成您的语言?"
|
||||||
@@ -28,32 +43,40 @@ about:
|
|||||||
feedback: "提供反馈"
|
feedback: "提供反馈"
|
||||||
sponsor: "赞助开发者"
|
sponsor: "赞助开发者"
|
||||||
|
|
||||||
|
popup:
|
||||||
|
refresh: "刷新"
|
||||||
|
include: "包括"
|
||||||
|
exclude: "排除"
|
||||||
|
advanced: "高级"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
title: "设置"
|
title: "设置"
|
||||||
length:
|
length:
|
||||||
title: "默认密码长度"
|
title: "默认密码长度"
|
||||||
hint: "建议长度:8-16"
|
hint: "建议长度:8-16"
|
||||||
quick_range: "快速调节长度范围"
|
quick_range: "快速调节长度范围"
|
||||||
include:
|
exclude_custom: "不要使用这些字符:"
|
||||||
title: "包括字符"
|
|
||||||
uppercase: "大写字母"
|
|
||||||
lowercase: "小写字母"
|
|
||||||
numeric: "数字"
|
|
||||||
special: "特殊"
|
|
||||||
exclude:
|
|
||||||
title: "排除字符"
|
|
||||||
similar: "相似"
|
|
||||||
ambiguous: "易混淆"
|
|
||||||
repeating:
|
|
||||||
title: "重复"
|
|
||||||
hint: "密码中的每个字符都将是独一无二的"
|
|
||||||
context_menu: "显示右键菜单"
|
|
||||||
|
|
||||||
generator:
|
advanced:
|
||||||
refresh: "刷新"
|
title: "高级密码生成器"
|
||||||
insert: "插入并复制"
|
saved_msg: "配置已保存"
|
||||||
options:
|
copied_msg: "已复制到剪贴板"
|
||||||
title: "快速调整"
|
actions:
|
||||||
hint: "这些调整将不会保存"
|
generate: "生成"
|
||||||
include: "包括"
|
save_config: "保存配置"
|
||||||
exclude: "排除"
|
copy_all: "复制全部"
|
||||||
|
password:
|
||||||
|
title: "密码生成器"
|
||||||
|
length: "密码长度"
|
||||||
|
min_of_type: "最少字符数"
|
||||||
|
passphrase:
|
||||||
|
title: "密码短语生成器"
|
||||||
|
length: "单词数量"
|
||||||
|
replace: "替换随机字符"
|
||||||
|
random_case: "随机大小写"
|
||||||
|
allow_repat:
|
||||||
|
label: "允许重复"
|
||||||
|
hint: "如果禁用,密码短语中的每个单词都是唯一的"
|
||||||
|
separate_words:
|
||||||
|
label: "用以下字符分隔单词"
|
||||||
|
hint: "如果启用,提供的序列将放置在每个单词之间。如果字段为空,将使用空格"
|
||||||
|
|||||||
Generated
+1463
-1707
File diff suppressed because it is too large
Load Diff
+14
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "password-generator",
|
"name": "password-generator",
|
||||||
"version": "4.1.1",
|
"version": "5.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -15,23 +15,24 @@
|
|||||||
"postinstall": "wxt prepare"
|
"postinstall": "wxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-components": "^9.56.3",
|
"@fluentui/react-components": "^9.57.0",
|
||||||
"@fluentui/react-icons": "^2.0.266",
|
"@fluentui/react-icons": "^2.0.270",
|
||||||
"@wxt-dev/i18n": "^0.2.3",
|
"@wxt-dev/i18n": "^0.2.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1",
|
||||||
|
"react-responsive": "^10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.13.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.16.0",
|
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
||||||
"@typescript-eslint/parser": "^8.16.0",
|
"@typescript-eslint/parser": "^8.19.1",
|
||||||
"@wxt-dev/module-react": "^1.1.2",
|
"@wxt-dev/module-react": "^1.1.3",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.12.0",
|
"globals": "^15.14.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.3",
|
||||||
"wxt": "^0.19.17"
|
"wxt": "^0.19.24"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { StorageProvider } from "@/utils/storage";
|
||||||
|
import { useTheme } from "@/utils/useTheme";
|
||||||
|
import { FluentProvider, makeStyles, Spinner, Theme } from "@fluentui/react-components";
|
||||||
|
import Snow from "./specials/Snow";
|
||||||
|
|
||||||
|
const App: React.FC<React.PropsWithChildren> = props =>
|
||||||
|
{
|
||||||
|
const theme: Theme = useTheme();
|
||||||
|
const cls = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FluentProvider theme={ theme }>
|
||||||
|
<StorageProvider loader={ <Spinner size="large" className={ cls.spinner } /> }>
|
||||||
|
{ props.children }
|
||||||
|
<Snow />
|
||||||
|
</StorageProvider>
|
||||||
|
</FluentProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
spinner:
|
||||||
|
{
|
||||||
|
height: "120px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
export const useStyles = makeStyles({
|
||||||
|
root:
|
||||||
|
{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr auto 1fr",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
label:
|
||||||
|
{
|
||||||
|
color: tokens.colorNeutralForeground2,
|
||||||
|
cursor: "pointer",
|
||||||
|
justifySelf: "start",
|
||||||
|
},
|
||||||
|
labelUnchecked:
|
||||||
|
{
|
||||||
|
"&:hover":
|
||||||
|
{
|
||||||
|
color: tokens.colorNeutralForeground2Hover,
|
||||||
|
|
||||||
|
"&:active":
|
||||||
|
{
|
||||||
|
color: tokens.colorNeutralForeground2Pressed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labelLeft:
|
||||||
|
{
|
||||||
|
justifySelf: "end",
|
||||||
|
textAlign: "right",
|
||||||
|
},
|
||||||
|
labelChecked:
|
||||||
|
{
|
||||||
|
fontWeight: tokens.fontWeightSemibold,
|
||||||
|
color: tokens.colorNeutralForeground1,
|
||||||
|
|
||||||
|
"&:hover":
|
||||||
|
{
|
||||||
|
color: tokens.colorNeutralForeground1Hover,
|
||||||
|
|
||||||
|
"&:active":
|
||||||
|
{
|
||||||
|
color: tokens.colorNeutralForeground1Pressed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Label, LabelProps, mergeClasses, Switch, SwitchOnChangeData, SwitchProps } from "@fluentui/react-components";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { useStyles } from "./DoubleLabeledSwitch.styles";
|
||||||
|
|
||||||
|
export default function DoubleLabledSwitch(props: DoubleLabledSwitchProps): ReactElement
|
||||||
|
{
|
||||||
|
const [isOn, setOn] = useState<boolean>(props.checked ?? props.defaultChecked ?? false);
|
||||||
|
const cls = useStyles();
|
||||||
|
const switchRef = useRef<HTMLInputElement | null>();
|
||||||
|
|
||||||
|
const setChecked = useCallback((checked: boolean) =>
|
||||||
|
{
|
||||||
|
if (!switchRef.current || isOn === checked)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switchRef.current.click();
|
||||||
|
}, [switchRef.current, isOn]);
|
||||||
|
|
||||||
|
const onChange = useCallback((ev: React.ChangeEvent<HTMLInputElement>, data: SwitchOnChangeData) =>
|
||||||
|
{
|
||||||
|
setOn(data.checked);
|
||||||
|
props.onChange?.(ev, data);
|
||||||
|
}, [props.onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div { ...props.outerRoot } className={ mergeClasses(cls.root, props.outerRoot?.className) }>
|
||||||
|
|
||||||
|
<Label
|
||||||
|
onClick={ () => setChecked(false) }
|
||||||
|
{ ...props.offLabelProps }
|
||||||
|
className={ mergeClasses(
|
||||||
|
cls.label,
|
||||||
|
cls.labelLeft,
|
||||||
|
!isOn ? cls.labelChecked : cls.labelUnchecked,
|
||||||
|
props.offLabelProps?.className
|
||||||
|
) }>
|
||||||
|
|
||||||
|
{ props.offLabel }
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Switch { ...props } ref={ (input) => switchRef.current = input } onChange={ onChange } />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
onClick={ () => setChecked(true) }
|
||||||
|
{ ...props.onLabelProps }
|
||||||
|
className={ mergeClasses(
|
||||||
|
cls.label,
|
||||||
|
isOn ? cls.labelChecked : cls.labelUnchecked,
|
||||||
|
props.onLabelProps?.className
|
||||||
|
) }>
|
||||||
|
|
||||||
|
{ props.onLabel }
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DoubleLabledSwitchProps = Omit<SwitchProps, "label"> &
|
||||||
|
{
|
||||||
|
offLabel?: string;
|
||||||
|
onLabel?: string;
|
||||||
|
outerRoot?: React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
offLabelProps?: LabelProps;
|
||||||
|
onLabelProps?: LabelProps;
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
@keyframes snowfall
|
||||||
|
{
|
||||||
|
0%
|
||||||
|
{
|
||||||
|
transform: translate3d(var(--left-start), 0, 0);
|
||||||
|
filter: opacity(.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
100%
|
||||||
|
{
|
||||||
|
transform: translate3d(var(--left-end), 610px, 0);
|
||||||
|
filter: opacity(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { mergeClasses } from "@fluentui/react-components";
|
import { mergeClasses } from "@fluentui/react-components";
|
||||||
import { SNOWFLAKES_NUM, useStyles } from "./Snow.styles";
|
import { SNOWFLAKES_NUM, useStyles } from "./Snow.styles";
|
||||||
|
import "./Snow.css";
|
||||||
|
|
||||||
const Snow: React.FC = () =>
|
const Snow: React.FC = () =>
|
||||||
{
|
{
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { GeneratorOptions } from "./storage";
|
|
||||||
|
|
||||||
const Characters =
|
|
||||||
{
|
|
||||||
Uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
||||||
Lowercase: "abcdefghijklmnopqrstuvwxyz",
|
|
||||||
Numeric: "1234567890",
|
|
||||||
Special: "!#$%&*+-=?@^_"
|
|
||||||
};
|
|
||||||
|
|
||||||
const Similar: string = "iIl1Lo0O";
|
|
||||||
const Ambiguous: string = "{}[]()/\\'\"`~,;:.<>";
|
|
||||||
|
|
||||||
export const CharacterHints = { ...Characters, Similar, Ambiguous };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a random password
|
|
||||||
* @param options Options for password generation
|
|
||||||
* @returns Randomly generated password
|
|
||||||
* @throws Error if options are invalid
|
|
||||||
*/
|
|
||||||
export function GeneratePassword(options: GeneratorOptions): string
|
|
||||||
{
|
|
||||||
ValidateOptions(options);
|
|
||||||
|
|
||||||
let password: string = GetRequiredCharacters(options);
|
|
||||||
const availableCharacters: string = GetAvailableCharacters(options);
|
|
||||||
|
|
||||||
for (let i = password.length; i < options.Length; i++)
|
|
||||||
{
|
|
||||||
const character: string = PickRandomFromArray(availableCharacters);
|
|
||||||
|
|
||||||
if (options.ExcludeRepeating && password.includes(character))
|
|
||||||
i--;
|
|
||||||
else
|
|
||||||
password += character;
|
|
||||||
}
|
|
||||||
|
|
||||||
password = ShuffleString(password);
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates options for password generation
|
|
||||||
* @param options Options for password generation
|
|
||||||
* @throws Error if options are invalid
|
|
||||||
*/
|
|
||||||
export function ValidateOptions(options: GeneratorOptions): void
|
|
||||||
{
|
|
||||||
if (options.Length < 4)
|
|
||||||
throw new Error(i18n.t("errors.too_short"));
|
|
||||||
|
|
||||||
const availableCharacters: string = GetAvailableCharacters(options);
|
|
||||||
|
|
||||||
if (availableCharacters.length < 1)
|
|
||||||
throw new Error(i18n.t("errors.no_characters"));
|
|
||||||
|
|
||||||
if (options.ExcludeRepeating && options.Length > availableCharacters.length)
|
|
||||||
throw new Error(i18n.t("errors.too_long"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a string containing all characters that are available for password generation
|
|
||||||
function GetAvailableCharacters(options: GeneratorOptions): string
|
|
||||||
{
|
|
||||||
let availableCharacters: string = "";
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(Characters))
|
|
||||||
if (options[key as keyof GeneratorOptions])
|
|
||||||
availableCharacters += value;
|
|
||||||
|
|
||||||
if (options.ExcludeSimilar)
|
|
||||||
availableCharacters = availableCharacters.replace(new RegExp(`[${Similar}]`, "g"), "");
|
|
||||||
|
|
||||||
if (options.Special && !options.ExcludeAmbiguous)
|
|
||||||
availableCharacters += Ambiguous;
|
|
||||||
|
|
||||||
return availableCharacters;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a string containing all characters from every available set that are required for password generation
|
|
||||||
function GetRequiredCharacters(options: GeneratorOptions): string
|
|
||||||
{
|
|
||||||
let result: string = "";
|
|
||||||
const characters: Record<string, string> = Object.assign({}, Characters);
|
|
||||||
|
|
||||||
if (!options.ExcludeAmbiguous)
|
|
||||||
characters.Special += Ambiguous;
|
|
||||||
|
|
||||||
if (options.ExcludeSimilar)
|
|
||||||
for (const key of Object.keys(characters))
|
|
||||||
characters[key] = characters[key].replace(new RegExp(`[${Similar}]`, "g"), "");
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(characters))
|
|
||||||
if (options[key as keyof GeneratorOptions])
|
|
||||||
result += PickRandomFromArray(value);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Picks a random character from a string
|
|
||||||
function PickRandomFromArray(array: string): string
|
|
||||||
{
|
|
||||||
return array[GetRandomInt(0, array.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/random
|
|
||||||
// min is inclusive, max is exclusive
|
|
||||||
function GetRandomInt(min: number, max: number): number
|
|
||||||
{
|
|
||||||
const arr = new Uint16Array(1);
|
|
||||||
crypto.getRandomValues(arr); // Using crypto instead of Math.random() as a CSPRNG
|
|
||||||
return Math.floor((arr[0] / 65_536) * (max - min)) + min;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffles a string using Fisher-Yates algorithm and CSPRNG
|
|
||||||
// See https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
|
|
||||||
function ShuffleString(str: string): string
|
|
||||||
{
|
|
||||||
const arr = str.split("");
|
|
||||||
|
|
||||||
for (let i = arr.length - 1; i > 0; i--)
|
|
||||||
{
|
|
||||||
const j = GetRandomInt(0, i + 1);
|
|
||||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
return arr.join("");
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
|||||||
|
// Based on ealamiLabs - Password generator (https://github.com/ealamiLabs/password-generator)
|
||||||
|
// licensed under MIT
|
||||||
|
|
||||||
|
import dictionary from "./dictionary.json";
|
||||||
|
import { getBooleanSequence, getRandomInt } from "./randomUtils";
|
||||||
|
|
||||||
|
/* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 ealamiLabs
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function generatePassphrase(options: PassphraseProps): string
|
||||||
|
{
|
||||||
|
const words: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < options.wordCount; i++)
|
||||||
|
{
|
||||||
|
const word: string = dictionary[getRandomInt(0, dictionary.length)].word;
|
||||||
|
|
||||||
|
if (!options.allowRepeating && words.includes(word))
|
||||||
|
i--;
|
||||||
|
else
|
||||||
|
words.push(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: string = words.join(options.separator).toLocaleLowerCase();
|
||||||
|
|
||||||
|
console.log(result);
|
||||||
|
|
||||||
|
if (options.swapCharacters)
|
||||||
|
result = swapCharacters(result);
|
||||||
|
|
||||||
|
if (options.randomizeCase)
|
||||||
|
result = RandomUpperCase(result);
|
||||||
|
|
||||||
|
console.log(result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RandomUpperCase(passphrase: string): string
|
||||||
|
{
|
||||||
|
const sequence: boolean[] = getBooleanSequence(passphrase.length);
|
||||||
|
let result: string = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < passphrase.length; i++)
|
||||||
|
result += sequence[i] ? passphrase[i].toLocaleUpperCase() : passphrase[i];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function swapCharacters(passphrase: string): string
|
||||||
|
{
|
||||||
|
const sequence: boolean[] = getBooleanSequence(passphrase.length);
|
||||||
|
let result: string = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < passphrase.length; i++)
|
||||||
|
if (sequence[i])
|
||||||
|
switch (passphrase[i].toLocaleLowerCase())
|
||||||
|
{
|
||||||
|
case "a":
|
||||||
|
result += getRandomInt(0, 100) < 50 ? "@" : "4";
|
||||||
|
break;
|
||||||
|
case "e":
|
||||||
|
result += "3";
|
||||||
|
break;
|
||||||
|
case "i":
|
||||||
|
result += "!";
|
||||||
|
break;
|
||||||
|
case "s":
|
||||||
|
result += getRandomInt(0, 100) < 50 ? "$" : "5";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result += passphrase[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
result += passphrase[i];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PassphraseProps =
|
||||||
|
{
|
||||||
|
wordCount: number;
|
||||||
|
swapCharacters: boolean;
|
||||||
|
randomizeCase: boolean;
|
||||||
|
allowRepeating: boolean;
|
||||||
|
separator: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { pickRandomFromArray, shuffleString } from "./randomUtils";
|
||||||
|
|
||||||
|
const Characters =
|
||||||
|
{
|
||||||
|
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
numeric: "1234567890",
|
||||||
|
special: "!#$%&*+-=?@^_"
|
||||||
|
};
|
||||||
|
|
||||||
|
const similar: string = "iIl1Lo0O";
|
||||||
|
const ambiguous: string = "{}[]()/\\'\"`~,;:.<>";
|
||||||
|
|
||||||
|
export const CharacterHints = { ...Characters, similar, ambiguous };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random password
|
||||||
|
* @param options Options for password generation
|
||||||
|
* @returns Randomly generated password
|
||||||
|
* @throws Error if options are invalid
|
||||||
|
*/
|
||||||
|
export function generatePassword(options: PasswordProps): string
|
||||||
|
{
|
||||||
|
validateOptions(options);
|
||||||
|
|
||||||
|
let password: string = getRequiredCharacters(options);
|
||||||
|
const availableCharacters: string = getAvailableCharacters(options);
|
||||||
|
|
||||||
|
for (let i = password.length; i < options.length; i++)
|
||||||
|
{
|
||||||
|
const character: string = pickRandomFromArray(availableCharacters);
|
||||||
|
|
||||||
|
if (options.excludeRepeating && password.includes(character))
|
||||||
|
i--;
|
||||||
|
else
|
||||||
|
password += character;
|
||||||
|
}
|
||||||
|
|
||||||
|
password = shuffleString(password);
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates options for password generation
|
||||||
|
* @param options Options for password generation
|
||||||
|
* @throws Error if options are invalid
|
||||||
|
*/
|
||||||
|
export function validateOptions(options: PasswordProps): void
|
||||||
|
{
|
||||||
|
if (options.length < 4)
|
||||||
|
throw new Error(i18n.t("errors.too_short"));
|
||||||
|
|
||||||
|
const availableCharacters: string = getAvailableCharacters(options);
|
||||||
|
|
||||||
|
if (availableCharacters.length < 1)
|
||||||
|
throw new Error(i18n.t("errors.no_characters"));
|
||||||
|
|
||||||
|
if (options.excludeRepeating && options.length > availableCharacters.length)
|
||||||
|
throw new Error(i18n.t("errors.too_long"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a string containing all characters that are available for password generation
|
||||||
|
function getAvailableCharacters(options: PasswordProps): string
|
||||||
|
{
|
||||||
|
let availableCharacters: string = "";
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(Characters))
|
||||||
|
if (options[key as keyof PasswordProps])
|
||||||
|
availableCharacters += value;
|
||||||
|
|
||||||
|
if (options.custom && options.customSet.length > 0)
|
||||||
|
availableCharacters += options.customSet;
|
||||||
|
|
||||||
|
if (options.excludeSimilar)
|
||||||
|
availableCharacters = availableCharacters.replace(new RegExp(`[${similar}]`, "g"), "");
|
||||||
|
|
||||||
|
if (options.special && !options.excludeAmbiguous)
|
||||||
|
availableCharacters += ambiguous;
|
||||||
|
|
||||||
|
if (options.excludeCustom.length > 0)
|
||||||
|
availableCharacters = availableCharacters.replace(new RegExp(`[${options.excludeCustom}]`, "g"), "");
|
||||||
|
|
||||||
|
return availableCharacters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a string containing all characters from every available set that are required for password generation
|
||||||
|
function getRequiredCharacters(options: PasswordProps): string
|
||||||
|
{
|
||||||
|
let result: string = "";
|
||||||
|
const characters: Record<string, string> = Object.assign({}, Characters);
|
||||||
|
|
||||||
|
if (!options.excludeAmbiguous)
|
||||||
|
characters.special += ambiguous;
|
||||||
|
|
||||||
|
if (options.custom && options.customSet.length > 0)
|
||||||
|
characters.custom = options.customSet;
|
||||||
|
|
||||||
|
for (const key of Object.keys(characters))
|
||||||
|
{
|
||||||
|
if (options.excludeSimilar)
|
||||||
|
characters[key] = characters[key].replace(new RegExp(`[${similar}]`, "g"), "");
|
||||||
|
|
||||||
|
if (options.excludeCustom.length > 0)
|
||||||
|
characters[key] = characters[key].replace(new RegExp(`[${options.excludeCustom}]`, "g"), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(characters))
|
||||||
|
if (options[key as keyof PasswordProps])
|
||||||
|
for (let i = 0; i < (options[key as keyof PasswordProps] as number); i++)
|
||||||
|
{
|
||||||
|
if (value.length < 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const char = pickRandomFromArray(value);
|
||||||
|
|
||||||
|
if (options.excludeRepeating && result.includes(char))
|
||||||
|
i--;
|
||||||
|
else
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PasswordProps =
|
||||||
|
{
|
||||||
|
length: number;
|
||||||
|
|
||||||
|
special: boolean | number;
|
||||||
|
numeric: boolean | number;
|
||||||
|
lowercase: boolean | number;
|
||||||
|
uppercase: boolean | number;
|
||||||
|
custom: boolean | number;
|
||||||
|
|
||||||
|
customSet: string;
|
||||||
|
|
||||||
|
excludeSimilar: boolean;
|
||||||
|
excludeAmbiguous: boolean;
|
||||||
|
|
||||||
|
excludeRepeating: boolean;
|
||||||
|
excludeCustom: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Picks a random character from a string
|
||||||
|
export function pickRandomFromArray(array: string): string
|
||||||
|
{
|
||||||
|
return array[getRandomInt(0, array.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/random
|
||||||
|
// min is inclusive, max is exclusive
|
||||||
|
export function getRandomInt(min: number, max: number): number
|
||||||
|
{
|
||||||
|
const arr = new Uint16Array(1);
|
||||||
|
crypto.getRandomValues(arr); // Using crypto instead of Math.random() as a CSPRNG
|
||||||
|
return Math.floor((arr[0] / 65_536) * (max - min)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffles a string using Fisher-Yates algorithm and CSPRNG
|
||||||
|
// See https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
|
||||||
|
export function shuffleString(str: string): string
|
||||||
|
{
|
||||||
|
const arr = str.split("");
|
||||||
|
|
||||||
|
for (let i = arr.length - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
const j = getRandomInt(0, i + 1);
|
||||||
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBooleanSequence(length: number): boolean[]
|
||||||
|
{
|
||||||
|
const arr = new Uint8Array(Math.ceil(length / 8));
|
||||||
|
crypto.getRandomValues(arr);
|
||||||
|
|
||||||
|
const result: boolean[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
const byte = arr[Math.floor(i / 8)];
|
||||||
|
const bit = byte & (1 << (i % 8));
|
||||||
|
result.push(bit !== 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { InfoLabel, Label, LabelProps, Slot } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
// FIXME: Remove ts-ignore comments once slots override fix is released
|
||||||
|
// Tracker: https://github.com/microsoft/fluentui/issues/27090
|
||||||
|
|
||||||
|
export default function infoLabel(label: string, hint: string): Slot<typeof Label>
|
||||||
|
{
|
||||||
|
// @ts-expect-error See FIXME
|
||||||
|
return {
|
||||||
|
children: (_: unknown, props: LabelProps) =>
|
||||||
|
<InfoLabel { ...props } info={ hint }>
|
||||||
|
{ label }
|
||||||
|
</InfoLabel>
|
||||||
|
};
|
||||||
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { Manifest } from "webextension-polyfill";
|
|||||||
export const personalLinks =
|
export const personalLinks =
|
||||||
{
|
{
|
||||||
website: "https://xfox111.net",
|
website: "https://xfox111.net",
|
||||||
twitter: "https://twitter.com/xfox111",
|
social: "https://bsky.app/profile/xfox111.net",
|
||||||
donation: "https://buymeacoffee.com/xfox111"
|
donation: "https://buymeacoffee.com/xfox111"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,4 @@ export default class ExtensionOptions
|
|||||||
{
|
{
|
||||||
public MinLength: number = 4;
|
public MinLength: number = 4;
|
||||||
public MaxLength: number = 32;
|
public MaxLength: number = 32;
|
||||||
public ContextMenu: boolean = true;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ export default class GeneratorOptions
|
|||||||
public Numeric: boolean = true;
|
public Numeric: boolean = true;
|
||||||
public Lowercase: boolean = true;
|
public Lowercase: boolean = true;
|
||||||
public Uppercase: boolean = true;
|
public Uppercase: boolean = true;
|
||||||
|
public Custom: boolean = false;
|
||||||
|
|
||||||
public ExcludeSimilar: boolean = true;
|
public ExcludeSimilar: boolean = true;
|
||||||
public ExcludeAmbiguous: boolean = true;
|
public ExcludeAmbiguous: boolean = true;
|
||||||
|
|
||||||
public ExcludeRepeating: boolean = false;
|
public ExcludeRepeating: boolean = false;
|
||||||
|
public ExcludeCustom: boolean = false;
|
||||||
|
|
||||||
|
public ExcludeCustomSet: string = "";
|
||||||
|
public IncludeCustomSet: string = "";
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ export default defineConfig({
|
|||||||
homepage_url: "https://github.com/xfox111/PasswordGeneratorExtension",
|
homepage_url: "https://github.com/xfox111/PasswordGeneratorExtension",
|
||||||
|
|
||||||
default_locale: "en",
|
default_locale: "en",
|
||||||
permissions: ["storage", "contextMenus", "activeTab"],
|
permissions: ["storage"],
|
||||||
|
|
||||||
icons:
|
icons:
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user