1
0
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:
Eugene Fox
2025-01-13 14:08:31 +03:00
committed by GitHub
parent c0cab1c0d1
commit 8126886fb5
62 changed files with 53646 additions and 2560 deletions
+2 -8
View File
@@ -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 -1
View File
@@ -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
+4 -5
View File
@@ -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
![Demo](https://cdn.xfox111.net/projects/pwdgen/demo.gif)
## Languages ## Languages
- Chinese (Simplified) - Chinese (Simplified)
@@ -93,4 +92,4 @@ If you are interested in fixing issues and contributing directly to the code bas
[![GitHub followers](https://img.shields.io/github/followers/xfox111?label=Follow%20@xfox111&style=social)](https://github.com/xfox111) [![GitHub followers](https://img.shields.io/github/followers/xfox111?label=Follow%20@xfox111&style=social)](https://github.com/xfox111)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-%40xfox111-orange)](https://buymeacoffee.com/xfox111) [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-%40xfox111-orange)](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)
+59
View File
@@ -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%",
},
});
+58
View File
@@ -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[];
};
+15
View File
@@ -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>
+51
View File
@@ -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);
}
+15
View File
@@ -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;
};
-24
View File
@@ -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"],
});
}
-22
View File
@@ -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,
},
});
+61
View File
@@ -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,
},
});
+128
View File
@@ -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()
};
+15
View File
@@ -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>
+65
View File
@@ -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);
}
+35
View File
@@ -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>
);
-30
View File
@@ -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,
}
},
});
-38
View File
@@ -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;
@@ -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",
}
}); });
+88
View File
@@ -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;
};
@@ -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);
}
}
+55 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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: 816" hint: "Recommended length: 816"
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
View File
@@ -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ść: 816" hint: "Zalecana długość: 816"
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
View File
@@ -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: 816" hint: "Comprimento recomendado: 816"
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
View File
@@ -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
View File
@@ -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
View File
@@ -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: "如果启用,提供的序列将放置在每个单词之间。如果字段为空,将使用空格"
+1463 -1707
View File
File diff suppressed because it is too large Load Diff
+14 -13
View File
@@ -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"
} }
} }
+28
View File
@@ -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;
+48
View File
@@ -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,
},
},
}
});
+65
View File
@@ -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;
};
+14
View File
@@ -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 = () =>
{ {
-128
View File
@@ -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
+108
View File
@@ -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;
};
+142
View File
@@ -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;
};
+46
View File
@@ -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;
}
+15
View File
@@ -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
View File
@@ -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"
}; };
-1
View File
@@ -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;
} }
+5 -1
View File
@@ -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
View File
@@ -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:
{ {