mirror of
https://github.com/XFox111/PasswordGeneratorExtension.git
synced 2026-04-22 08:08:01 +03:00
Major 2.0 (#8)
* Migrated to React 18 and FluentUI 9 * Added Ukranian translation * Updated GitHub templates * Updated CI/CD - Added CodeQL and Dependabot pipelines - Removed Whitesource Bolt integration - Added PR pipeline - Update release pipeline to meet ReactJS - Added Edge publish to pipeline - Updated PR checklist * Updated repo docs * Moved dependabot yml to the right place * Update README.md * Added path filters to pipelines
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
main
|
||||
{
|
||||
width: 400px;
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Buy me a coffee button style
|
||||
.fui-Button.bmc
|
||||
{
|
||||
background-color: var(--colorPaletteDarkOrangeBorder2) !important;
|
||||
|
||||
&:hover
|
||||
{
|
||||
background-color: var(--colorPaletteDarkOrangeForeground1) !important;
|
||||
}
|
||||
|
||||
&:active
|
||||
{
|
||||
background-color: var(--colorPaletteDarkOrangeBackground2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Overrides for default FluentUI styles
|
||||
a.fui-Button
|
||||
{
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.fui-Accordion
|
||||
{
|
||||
section
|
||||
{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.fui-AccordionHeader__expandIcon > svg
|
||||
{
|
||||
transition-duration: .5s;
|
||||
transition-timing-function: var(--curveDecelerateMax);
|
||||
}
|
||||
}
|
||||
|
||||
.scaleUpIn
|
||||
{
|
||||
animation-name: scaleUpInAnim;
|
||||
animation-timing-function: var(--curveEasyEaseMax);
|
||||
animation-duration: .5s;
|
||||
}
|
||||
@keyframes scaleUpInAnim
|
||||
{
|
||||
from
|
||||
{
|
||||
transform: scale(.5);
|
||||
opacity: 0;
|
||||
}
|
||||
to
|
||||
{
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.spin
|
||||
{
|
||||
animation-name: spinAnim;
|
||||
animation-timing-function: var(--curveEasyEaseMax);
|
||||
animation-duration: .5s;
|
||||
}
|
||||
@keyframes spinAnim
|
||||
{
|
||||
from
|
||||
{
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to
|
||||
{
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fadeIn
|
||||
{
|
||||
animation-name: fadeInAnim;
|
||||
animation-timing-function: var(--curveDecelerateMin);
|
||||
animation-duration: .5s;
|
||||
}
|
||||
@keyframes fadeInAnim
|
||||
{
|
||||
from
|
||||
{
|
||||
opacity: 0;
|
||||
}
|
||||
to
|
||||
{
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stack
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.horizontal
|
||||
{
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&.gap
|
||||
{
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import { Accordion, FluentProvider, Text, Theme, Title2, webDarkTheme, webLightTheme } from "@fluentui/react-components";
|
||||
import "./App.scss";
|
||||
import SettingsSection from "./Components/SettingsSection";
|
||||
import AboutSection from "./Components/AboutSection";
|
||||
import Package from "../package.json";
|
||||
import PasswordView from "./Components/PasswordView";
|
||||
import Settings from "./Utils/Settings";
|
||||
import GeneratorOptions from "./Utils/GeneratorOptions";
|
||||
import { loc } from "./Utils/Localization";
|
||||
|
||||
interface IStates
|
||||
{
|
||||
theme: Theme;
|
||||
generatorOptions: GeneratorOptions;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
interface IProps
|
||||
{
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export default class App extends React.Component<IProps, IStates>
|
||||
{
|
||||
constructor(props: IProps)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
theme: this.UpdateTheme(),
|
||||
generatorOptions: new GeneratorOptions(),
|
||||
settings: props.settings
|
||||
};
|
||||
|
||||
Settings.OnChanged = (changes) => this.setState({ settings: { ...this.state.settings, ...changes } });
|
||||
GeneratorOptions.OnChanged = (changes) => this.setState({ generatorOptions: { ...this.state.generatorOptions, ...changes } });
|
||||
}
|
||||
|
||||
public async componentDidMount(): Promise<void>
|
||||
{
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change",
|
||||
(arg: MediaQueryListEvent) =>
|
||||
this.setState({ theme: this.UpdateTheme(arg.matches) })
|
||||
);
|
||||
|
||||
this.setState({ generatorOptions: await GeneratorOptions.Init() });
|
||||
}
|
||||
|
||||
private UpdateTheme(isDark?: boolean): Theme
|
||||
{
|
||||
let theme: Theme = (isDark ?? window.matchMedia("(prefers-color-scheme: dark)").matches) ? webDarkTheme : webLightTheme;
|
||||
document.body.style.backgroundColor = theme.colorNeutralBackground1;
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
public render(): JSX.Element
|
||||
{
|
||||
return (
|
||||
<FluentProvider theme={ this.state.theme }>
|
||||
<main className="stack gap">
|
||||
<header className="stack horizontal gap">
|
||||
<Title2 as="h1">{ loc("Password generator") }</Title2>
|
||||
<Text as="span">v{ Package.version }</Text>
|
||||
</header>
|
||||
|
||||
<PasswordView settings={ this.state.settings } generatorOptions={ this.state.generatorOptions } />
|
||||
|
||||
<Accordion collapsible>
|
||||
<SettingsSection
|
||||
generatorOptions={ this.state.generatorOptions }
|
||||
settings={ this.state.settings } />
|
||||
<AboutSection />
|
||||
</Accordion>
|
||||
</main>
|
||||
</FluentProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,54 @@
|
||||
import { AccordionItem, AccordionHeader, AccordionPanel, Link, Text, Button } from "@fluentui/react-components";
|
||||
import { InfoRegular, PersonFeedbackRegular } from "@fluentui/react-icons";
|
||||
import { ReactComponent as BuyMeACoffee } from "../Assets/BuyMeACoffee.svg";
|
||||
import React from "react";
|
||||
import { loc } from "../Utils/Localization";
|
||||
|
||||
export default class AboutSection extends React.Component
|
||||
{
|
||||
public render(): JSX.Element
|
||||
{
|
||||
return (
|
||||
<AccordionItem value="about">
|
||||
<AccordionHeader as="h2" icon={ <InfoRegular /> }>{ loc("About") }</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<section className="stack gap fadeIn">
|
||||
<Text as="p">
|
||||
{ loc("Developed by Eugene Fox") } (<Link href="https://twitter.com/xfox111" target="_blank">@xfox111</Link>)
|
||||
<br />
|
||||
{ loc("Licensed under") } <Link href="https://github.com/XFox111/PasswordGeneratorExtension/blob/master/LICENSE" target="_blank">{ loc("MIT license") }</Link>
|
||||
</Text>
|
||||
<Text as="p">
|
||||
{ loc("Want to contribute translation for your language?") } <Link href="https://github.com/XFox111/PasswordGeneratorExtension/blob/master/CONTRIBUTING.md" target="_blank">{ loc("Read this to get started") }</Link>
|
||||
</Text>
|
||||
<Text as="p">
|
||||
<Link href="https://xfox111.net/" target="_blank">{ loc("My website") }</Link>
|
||||
<br />
|
||||
<Link href="https://github.com/xfox111/PasswordGeneratorExtension" target="_blank">{ loc("Source code") }</Link>
|
||||
<br />
|
||||
<Link href="https://github.com/XFox111/PasswordGeneratorExtension/releases/latest" target="_blank">{ loc("Changelog") }</Link>
|
||||
</Text>
|
||||
|
||||
<div className="stack horizontal gap">
|
||||
<Button
|
||||
as="a" target="_blank"
|
||||
href="mailto:feedback@xfox111.net"
|
||||
appearance="primary" icon={ <PersonFeedbackRegular /> }>
|
||||
|
||||
{ loc("Leave feedback") }
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
as="a" target="_blank"
|
||||
href="https://buymeacoffee.com/xfox111"
|
||||
className="bmc" appearance="primary" icon={ <BuyMeACoffee /> }>
|
||||
|
||||
{ loc("Buy me a coffee") }
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Button, Text } from "@fluentui/react-components";
|
||||
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, Table, TableHeader, TableRow, TableHeaderCell, TableBody, TableCell, DialogActions } from "@fluentui/react-components/unstable";
|
||||
import { QuestionCircleRegular } from "@fluentui/react-icons";
|
||||
import React from "react";
|
||||
import Generator from "../Utils/Generator";
|
||||
import { loc } from "../Utils/Localization";
|
||||
|
||||
export default class CharacterHelpDialog extends React.Component
|
||||
{
|
||||
public render(): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button appearance="subtle" style={ { marginLeft: 5 } } icon={ <QuestionCircleRegular /> } />
|
||||
</DialogTrigger>
|
||||
<DialogSurface aria-label="label">
|
||||
<DialogTitle>{ loc("Character options") }</DialogTitle>
|
||||
<DialogBody>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>{ loc("Set_name") }</TableHeaderCell>
|
||||
<TableHeaderCell>{ loc("Characters") }</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Lowercase") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.Lowercase.substring(0, 10) }{ loc(", etc.") }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Uppercase") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.Uppercase.substring(0, 10) }{ loc(", etc.") }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Numeric") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.Numeric }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Special symbols") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.SpecialCharacters }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Ambiguous") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.AmbiguousCharacters }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Similar") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.SimilarCharacters }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DialogBody>
|
||||
<DialogActions>
|
||||
<DialogTrigger>
|
||||
<Button appearance="secondary">{ loc("OK") }</Button>
|
||||
</DialogTrigger>
|
||||
</DialogActions>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { Input, Button, Link, Text, Tooltip } from "@fluentui/react-components";
|
||||
import { Alert } from "@fluentui/react-components/unstable";
|
||||
import { ArrowClockwiseRegular, CheckmarkRegular, CopyRegular } from "@fluentui/react-icons";
|
||||
import React from "react";
|
||||
import Generator from "../Utils/Generator";
|
||||
import GeneratorOptions from "../Utils/GeneratorOptions";
|
||||
import { loc } from "../Utils/Localization";
|
||||
import Settings from "../Utils/Settings";
|
||||
|
||||
interface IStates
|
||||
{
|
||||
password: string;
|
||||
error?: string;
|
||||
copyIcon: JSX.Element;
|
||||
}
|
||||
|
||||
interface IProps
|
||||
{
|
||||
generatorOptions: GeneratorOptions;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export default class PasswordView extends React.Component<IProps, IStates>
|
||||
{
|
||||
constructor(props: IProps)
|
||||
{
|
||||
super(props);
|
||||
this.state =
|
||||
{
|
||||
password: Generator.GeneratePassword(props.generatorOptions),
|
||||
error: Generator.ValidateProps(props.generatorOptions),
|
||||
copyIcon: <CopyRegular className="scaleUpIn" />,
|
||||
};
|
||||
}
|
||||
|
||||
private OnCopyPassword(password : string): void
|
||||
{
|
||||
console.log("PasswordView.OnCopyPassword");
|
||||
|
||||
if (!document.hasFocus())
|
||||
return;
|
||||
|
||||
window.navigator.clipboard.writeText(password);
|
||||
|
||||
this.setState({ copyIcon: <CheckmarkRegular className="scaleUpIn" /> });
|
||||
setTimeout(() => this.setState({ copyIcon: <CopyRegular className="scaleUpIn" /> }), 3000);
|
||||
}
|
||||
|
||||
private OnRefreshPassword(): void
|
||||
{
|
||||
console.log("PasswordView.OnRefreshPassword");
|
||||
|
||||
let password : string = Generator.GeneratePassword(this.props.generatorOptions);
|
||||
|
||||
this.setState({ password });
|
||||
|
||||
document.querySelector("svg#refresh-btn")?.classList.add("spin");
|
||||
setTimeout(() => document.querySelector("svg#refresh-btn")?.classList.remove("spin"), 600);
|
||||
|
||||
if (this.props.settings.Autocopy)
|
||||
this.OnCopyPassword(password);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>): void
|
||||
{
|
||||
console.log("PasswordView.componentDidUpdate");
|
||||
|
||||
// Converting to JSON is the easiest way to compare objects
|
||||
if (JSON.stringify(prevProps.generatorOptions) === JSON.stringify(this.props.generatorOptions))
|
||||
return;
|
||||
|
||||
let error : string = Generator.ValidateProps(this.props.generatorOptions);
|
||||
let password = Generator.GeneratePassword(this.props.generatorOptions);
|
||||
|
||||
this.setState({ password, error });
|
||||
|
||||
if (!error && this.props.settings.Autocopy)
|
||||
this.OnCopyPassword(password);
|
||||
}
|
||||
|
||||
private AlterSpecialsOnce(useSpecials : boolean) : void
|
||||
{
|
||||
console.log("PasswordView.AlterSpecialsOnce", `useSpecials: ${useSpecials}`);
|
||||
|
||||
let options : GeneratorOptions = { ...this.props.generatorOptions, Special: useSpecials, ExcludeAmbiguous: true };
|
||||
|
||||
let error : string = Generator.ValidateProps(options);
|
||||
let password : string = Generator.GeneratePassword(options);
|
||||
|
||||
this.setState({ password, error });
|
||||
|
||||
if (error)
|
||||
setTimeout(() => this.OnRefreshPassword(), 3000);
|
||||
}
|
||||
|
||||
public render(): JSX.Element
|
||||
{
|
||||
return this.state.error ?
|
||||
<Alert intent="error" className="fadeIn">{ this.state.error }</Alert>
|
||||
:
|
||||
<section className="stack fadeIn">
|
||||
<Input
|
||||
value={ this.state.password } readOnly
|
||||
contentAfter={
|
||||
<>
|
||||
<Tooltip content={ loc("Copy") } relationship="label">
|
||||
<Button onClick={ () => this.OnCopyPassword(this.state.password) } appearance="subtle" icon={ this.state.copyIcon } />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={ loc("Generate new") } relationship="label">
|
||||
<Button onClick={ () => this.OnRefreshPassword() } appearance="subtle" icon={ <ArrowClockwiseRegular id="refresh-btn" /> } />
|
||||
</Tooltip>
|
||||
</>
|
||||
} />
|
||||
<Text>
|
||||
{ this.props.generatorOptions.Special ?
|
||||
<Link onClick={ () => this.AlterSpecialsOnce(false) }>
|
||||
{ loc("Exclude special symbols one time") }
|
||||
</Link>
|
||||
:
|
||||
<Link onClick={ () => this.AlterSpecialsOnce(true) }>
|
||||
{ loc("Include special symbols one time") }
|
||||
</Link>
|
||||
}
|
||||
</Text>
|
||||
</section>
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { AccordionItem, AccordionHeader, AccordionPanel, Label, Text, Input, Divider, Checkbox, Tooltip } from "@fluentui/react-components";
|
||||
import { QuestionCircleRegular, SettingsRegular } from "@fluentui/react-icons";
|
||||
import React from "react";
|
||||
import GeneratorOptions from "../Utils/GeneratorOptions";
|
||||
import { loc } from "../Utils/Localization";
|
||||
import Settings from "../Utils/Settings";
|
||||
import CharacterHelpDialog from "./CharacterHelpDialog";
|
||||
|
||||
interface IProps
|
||||
{
|
||||
generatorOptions: GeneratorOptions;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export default class SettingsSection extends React.Component<IProps>
|
||||
{
|
||||
public render(): JSX.Element
|
||||
{
|
||||
let options : GeneratorOptions = this.props.generatorOptions;
|
||||
let settings : Settings = this.props.settings;
|
||||
|
||||
return (
|
||||
<AccordionItem value="settings">
|
||||
<AccordionHeader as="h2" icon={ <SettingsRegular /> }>{ loc("Settings") }</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<section className="stack gap fadeIn">
|
||||
<Label weight="semibold" htmlFor="pwd-length">{ loc("Password length") }</Label>
|
||||
<div className="stack">
|
||||
<Input
|
||||
id="pwd-length"
|
||||
value={ options.Length.toString() }
|
||||
onChange={ (_, e) => GeneratorOptions.Update({ Length: parseInt(e.value) }) }
|
||||
type="number" min={ 4 } />
|
||||
<Text size={ 200 }>{ loc("Recommended password length") } <b>16-32</b></Text>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="stack">
|
||||
<Text as="h3" weight="semibold">
|
||||
{ loc("Character options") }
|
||||
<CharacterHelpDialog />
|
||||
</Text>
|
||||
|
||||
<Text as="h4">{ loc("Include") }</Text>
|
||||
<div className="stack horizontal">
|
||||
<Checkbox label={ loc("Special symbols") }
|
||||
checked={ options.Special } onChange={ (_, e) => GeneratorOptions.Update({ Special: e.checked as boolean }) } />
|
||||
<Checkbox label={ loc("Numeric") }
|
||||
checked={ options.Numeric } onChange={ (_, e) => GeneratorOptions.Update({ Numeric: e.checked as boolean }) } />
|
||||
<Checkbox label={ loc("Uppercase") }
|
||||
checked={ options.Uppercase } onChange={ (_, e) => GeneratorOptions.Update({ Uppercase: e.checked as boolean }) } />
|
||||
<Checkbox label={ loc("Lowercase") }
|
||||
checked={ options.Lowercase } onChange={ (_, e) => GeneratorOptions.Update({ Lowercase: e.checked as boolean }) } />
|
||||
</div>
|
||||
|
||||
<Text as="h4">{ loc("Exclude") }</Text>
|
||||
<div className="stack horizontal">
|
||||
<Checkbox label={ loc("Similar") }
|
||||
checked={ options.ExcludeSimilar } onChange={ (_, e) => GeneratorOptions.Update({ ExcludeSimilar: e.checked as boolean }) } />
|
||||
<Checkbox label={ loc("Ambiguous") }
|
||||
checked={ options.ExcludeAmbiguous } onChange={ (_, e) => GeneratorOptions.Update({ ExcludeAmbiguous: e.checked as boolean }) } />
|
||||
<Checkbox label={ loc("Repeating") }
|
||||
checked={ options.ExcludeRepeating } onChange={ (_, e) => GeneratorOptions.Update({ ExcludeRepeating: e.checked as boolean }) } />
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="stack">
|
||||
<div>
|
||||
<Tooltip content={ loc("Right-click password field to quickly generate password") } relationship="description">
|
||||
<Checkbox label={ <Text>{loc("Add shortcut to context menu")} <QuestionCircleRegular /></Text> }
|
||||
checked={ settings.AddContext } onChange={ (_, e) => Settings.Update({ AddContext: e.checked as boolean }) } />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Checkbox label={ loc("Automatically copy to clipboard") }
|
||||
checked={ settings.Autocopy } onChange={ (_, e) => Settings.Update({ Autocopy: e.checked as boolean }) } />
|
||||
</div>
|
||||
</section>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// BackgroundService.ts
|
||||
// Background script that handles the context menu visibility
|
||||
|
||||
import { loc } from "../Utils/Localization";
|
||||
|
||||
function UpdateContextMenu(isEnabled: boolean) : void
|
||||
{
|
||||
console.log("BackgroundService.UpdateContextMenu", isEnabled);
|
||||
chrome.contextMenus.update("generatePassword", { visible: isEnabled });
|
||||
}
|
||||
|
||||
async function OnContextClick(info : chrome.contextMenus.OnClickData) : Promise<void>
|
||||
{
|
||||
console.log("BackgroundService.OnContextClick", info);
|
||||
|
||||
let tabInfo : chrome.tabs.Tab[] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
console.log("BackgroundService.OnContextClick", tabInfo);
|
||||
|
||||
chrome.tabs.sendMessage<string>(tabInfo[0].id, info.menuItemId as string);
|
||||
}
|
||||
|
||||
if (!chrome.runtime.onInstalled.hasListeners())
|
||||
chrome.runtime.onInstalled.addListener(async () =>
|
||||
{
|
||||
console.log("[BackgroundService] chrome.runtime.onInstalled");
|
||||
chrome.contextMenus.removeAll();
|
||||
|
||||
chrome.contextMenus.create(
|
||||
{
|
||||
title: loc("Quick generate password"),
|
||||
contexts: [ "editable" ],
|
||||
id: "generatePassword"
|
||||
}
|
||||
);
|
||||
|
||||
let settings : { [key : string]: any } = await chrome.storage.sync.get({ AddContext: true });
|
||||
|
||||
UpdateContextMenu(settings.AddContext);
|
||||
});
|
||||
|
||||
if (!chrome.contextMenus.onClicked.hasListeners())
|
||||
chrome.contextMenus.onClicked.addListener(OnContextClick);
|
||||
|
||||
if (!chrome.storage.sync.onChanged.hasListeners())
|
||||
chrome.storage.sync.onChanged.addListener(changes =>
|
||||
{
|
||||
console.log("[BackgroundService] chrome.storage.sync.onChanged", changes);
|
||||
if (changes.AddContext?.newValue !== undefined)
|
||||
UpdateContextMenu(changes.AddContext.newValue);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// ContentService.ts
|
||||
// Content script that handles quick password generation through context menu
|
||||
|
||||
import Generator from "../Utils/Generator";
|
||||
import GeneratorOptions from "../Utils/GeneratorOptions";
|
||||
import { loc } from "../Utils/Localization";
|
||||
|
||||
if (!chrome.runtime.onMessage.hasListeners())
|
||||
chrome.runtime.onMessage.addListener(async message =>
|
||||
{
|
||||
console.log("[ContentService] chrome.runtime.onMessage", message);
|
||||
|
||||
if (message === "generatePassword")
|
||||
{
|
||||
let generatorOptions : GeneratorOptions = await GeneratorOptions.Init();
|
||||
let password : string = Generator.GeneratePassword(generatorOptions);
|
||||
|
||||
let input : HTMLInputElement = document.activeElement as HTMLInputElement;
|
||||
|
||||
if (![ "INPUT", "TEXTAREA" ].includes(input.tagName))
|
||||
return;
|
||||
|
||||
console.log("[ContentService] chrome.runtime.onMessage", input);
|
||||
|
||||
if (input.tagName !== "INPUT" || input.readOnly || ![ "text", "password" ].includes(input.type))
|
||||
{
|
||||
window.alert(loc("Quick generator is only available on password fields"));
|
||||
return;
|
||||
}
|
||||
|
||||
input.focus();
|
||||
input.value = password;
|
||||
window.navigator.clipboard.writeText(password);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[ContentService] Loaded");
|
||||
@@ -0,0 +1,113 @@
|
||||
import GeneratorOptions from "./GeneratorOptions";
|
||||
import { loc } from "./Localization";
|
||||
|
||||
export default class Generator
|
||||
{
|
||||
public static Uppercase = "ABCDEFGHJKMNPQRSTUVWXYZ";
|
||||
public static Lowercase = this.Uppercase.toLowerCase();
|
||||
public static Numeric = "23456789";
|
||||
public static SpecialCharacters = "!#$%&*+-=?@^_";
|
||||
public static AmbiguousCharacters = "{}[]()/\\'\"`~,;:.<>";
|
||||
public static SimilarCharacters = "il1Lo0O";
|
||||
|
||||
public static GeneratePassword(props : GeneratorOptions) : string
|
||||
{
|
||||
// Validating parameters
|
||||
if (this.ValidateProps(props))
|
||||
return "";
|
||||
|
||||
// Generating password
|
||||
let availableCharacters : string = this.GetAvailableCharacters(props);
|
||||
let requiredCharacters : string = this.GetRequiredCharacters(props);
|
||||
|
||||
let password : string = "";
|
||||
|
||||
for (let i = 0; i < props.Length; i++)
|
||||
{
|
||||
let char : string = this.PickRandomFromArray(availableCharacters);
|
||||
|
||||
if (props.ExcludeRepeating && password.includes(char))
|
||||
i--;
|
||||
else
|
||||
password += char;
|
||||
}
|
||||
|
||||
for (let i = 0; i < requiredCharacters.length; i++)
|
||||
{
|
||||
if (props.ExcludeRepeating && password.includes(requiredCharacters[i]))
|
||||
continue;
|
||||
|
||||
password = password.replace(this.PickRandomFromArray(password), requiredCharacters[i]);
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
public static ValidateProps(props : GeneratorOptions): string
|
||||
{
|
||||
if (!props.Lowercase && !props.Uppercase)
|
||||
return loc("Either lowercase or uppercase characters must be included");
|
||||
|
||||
let availableCharacters : string = this.GetAvailableCharacters(props);
|
||||
|
||||
if (props.ExcludeRepeating && availableCharacters.length < props.Length)
|
||||
return loc("Selected length is too long to exclude repeating characters");
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static GetAvailableCharacters(props : GeneratorOptions) : string
|
||||
{
|
||||
let availableCharacters : string = "";
|
||||
|
||||
if (props.Special)
|
||||
availableCharacters += this.SpecialCharacters;
|
||||
if (props.Numeric)
|
||||
availableCharacters += this.Numeric;
|
||||
if (props.Lowercase)
|
||||
availableCharacters += this.Lowercase;
|
||||
if (props.Uppercase)
|
||||
availableCharacters += this.Uppercase;
|
||||
|
||||
if (!props.ExcludeAmbiguous)
|
||||
availableCharacters += this.AmbiguousCharacters;
|
||||
if (!props.ExcludeSimilar)
|
||||
availableCharacters += this.SimilarCharacters;
|
||||
|
||||
return availableCharacters;
|
||||
}
|
||||
|
||||
private static GetRequiredCharacters(props : GeneratorOptions) : string
|
||||
{
|
||||
let requiredCharacters : string = "";
|
||||
|
||||
if (props.Special)
|
||||
requiredCharacters += this.PickRandomFromArray(this.SpecialCharacters);
|
||||
if (props.Numeric)
|
||||
requiredCharacters += this.PickRandomFromArray(this.Numeric);
|
||||
if (props.Lowercase)
|
||||
requiredCharacters += this.PickRandomFromArray(this.Lowercase);
|
||||
if (props.Uppercase)
|
||||
requiredCharacters += this.PickRandomFromArray(this.Uppercase);
|
||||
|
||||
if (!props.ExcludeAmbiguous)
|
||||
requiredCharacters += this.PickRandomFromArray(this.AmbiguousCharacters);
|
||||
if (!props.ExcludeSimilar)
|
||||
requiredCharacters += this.PickRandomFromArray(this.SimilarCharacters);
|
||||
|
||||
return requiredCharacters;
|
||||
}
|
||||
|
||||
// See https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/random
|
||||
// min is inclusive, max is exclusive
|
||||
private static GetRandomInt(min : number, max : number) : number
|
||||
{
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
private static PickRandomFromArray(array : string) : string
|
||||
{
|
||||
return array[this.GetRandomInt(0, array.length)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
export default class GeneratorOptions
|
||||
{
|
||||
public Length: number = 16;
|
||||
|
||||
public Special: boolean = true;
|
||||
public Numeric: boolean = true;
|
||||
public Lowercase: boolean = true;
|
||||
public Uppercase: boolean = true;
|
||||
|
||||
public ExcludeSimilar: boolean = true;
|
||||
public ExcludeAmbiguous: boolean = true;
|
||||
public ExcludeRepeating: boolean = false;
|
||||
|
||||
public static OnChanged : (changes : Partial<GeneratorOptions>) => void;
|
||||
|
||||
public static async Init() : Promise<GeneratorOptions>
|
||||
{
|
||||
let fallbackOptions : GeneratorOptions = new GeneratorOptions();
|
||||
|
||||
if (!chrome?.storage?.sync) // Extension is running as a standalone app
|
||||
return fallbackOptions;
|
||||
|
||||
let props : { [key: string]: any } = await chrome.storage.sync.get(fallbackOptions);
|
||||
|
||||
chrome.storage.sync.onChanged.addListener(GeneratorOptions.OnStorageChanged);
|
||||
|
||||
return props as GeneratorOptions;
|
||||
}
|
||||
|
||||
public static async Update(changes : Partial<GeneratorOptions>) : Promise<void>
|
||||
{
|
||||
if (chrome?.storage?.sync)
|
||||
await chrome?.storage?.sync?.set(changes);
|
||||
else
|
||||
GeneratorOptions.OnChanged(changes);
|
||||
}
|
||||
|
||||
private static OnStorageChanged(changes : { [key: string]: chrome.storage.StorageChange }) : void
|
||||
{
|
||||
let propsList : string[] = Object.keys(new GeneratorOptions());
|
||||
let options : { [key: string]: any } = { };
|
||||
|
||||
Object.entries(changes)
|
||||
.filter(i => propsList.includes(i[0]))
|
||||
.map(i => options[i[0]] = i[1].newValue);
|
||||
|
||||
if (GeneratorOptions.OnChanged)
|
||||
GeneratorOptions.OnChanged(options as Partial<GeneratorOptions>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export default class Localization
|
||||
{
|
||||
public static GetString(key : string) : string
|
||||
{
|
||||
let sanitizedKey : string = key
|
||||
.replaceAll(".", "_")
|
||||
.replaceAll(",", "_")
|
||||
.replaceAll(" ", "_")
|
||||
.replaceAll("-", "_")
|
||||
.replaceAll("?", "_")
|
||||
.replaceAll("!", "_");
|
||||
|
||||
let str : string = chrome?.i18n?.getMessage(sanitizedKey);
|
||||
|
||||
return str ?? key;
|
||||
}
|
||||
}
|
||||
|
||||
export function loc(key : string) : string
|
||||
{
|
||||
return Localization.GetString(key);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
export default class Settings
|
||||
{
|
||||
public AddContext : boolean = true;
|
||||
public Autocopy : boolean = true;
|
||||
|
||||
public static OnChanged : (changes : Partial<Settings>) => void;
|
||||
|
||||
public static async Init() : Promise<Settings>
|
||||
{
|
||||
let fallbackOptions = new Settings();
|
||||
|
||||
if (!chrome?.storage?.sync)
|
||||
return fallbackOptions;
|
||||
|
||||
let props : { [key: string]: any } = await chrome.storage.sync.get(fallbackOptions);
|
||||
|
||||
chrome.storage.sync.onChanged.addListener(Settings.OnStorageChanged);
|
||||
|
||||
return props as Settings;
|
||||
}
|
||||
|
||||
public static async Update(changes : Partial<Settings>) : Promise<void>
|
||||
{
|
||||
if (chrome?.storage?.sync)
|
||||
await chrome?.storage?.sync?.set(changes);
|
||||
else
|
||||
Settings.OnChanged(changes);
|
||||
}
|
||||
|
||||
private static OnStorageChanged(changes : { [key: string]: chrome.storage.StorageChange }) : void
|
||||
{
|
||||
let propsList : string[] = Object.keys(new Settings());
|
||||
let settings : { [key: string]: any } = { };
|
||||
|
||||
Object.entries(changes)
|
||||
.filter(i => propsList.includes(i[0]))
|
||||
.map(i => settings[i[0]] = i[1].newValue);
|
||||
|
||||
if (Settings.OnChanged)
|
||||
Settings.OnChanged(settings as Partial<Settings>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import Settings from "./Utils/Settings";
|
||||
|
||||
Settings.Init().then(settings =>
|
||||
ReactDOM
|
||||
.createRoot(document.querySelector("#root") as HTMLElement)
|
||||
.render(<App settings={ settings } />));
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
Reference in New Issue
Block a user