Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3d635c985 | |||
| 9423b36b35 | |||
| 99e72869e5 | |||
| 75b6ccd2d6 | |||
| 96c8a6a8d6 | |||
| 8126886fb5 | |||
| c0cab1c0d1 | |||
| acac03feab | |||
| 7db0aa5f24 | |||
| 016e3bfba0 | |||
| 56ae964c04 | |||
| f4674265e4 | |||
| b4117e430f | |||
| 3ecb6c4a31 |
@@ -21,5 +21,5 @@
|
||||
}
|
||||
},
|
||||
|
||||
"postCreateCommand": "yarn install"
|
||||
"postCreateCommand": "npm install"
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
module.exports = {
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect",
|
||||
},
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"files": [
|
||||
".eslintrc.{js,cjs}"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
}
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"react/react-in-jsx-scope": "off"
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Questions & Discussions
|
||||
|
||||
@@ -31,3 +31,15 @@ updates:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
target-branch: "next"
|
||||
assignees:
|
||||
- "xfox111"
|
||||
reviewers:
|
||||
- "xfox111"
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- ⚠️ Make sure that you create this PR against `next` branch and not `main` -->
|
||||
|
||||
## Description
|
||||
<!--Put short description of the pull request here-->
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
types: [ released ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bypass_audit:
|
||||
description: Bypass npm audit
|
||||
type: boolean
|
||||
default: false
|
||||
targets:
|
||||
description: Targets
|
||||
required: true
|
||||
@@ -43,33 +47,25 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
|
||||
- run: yarn install
|
||||
- run: yarn lint
|
||||
- run: TARGET=${{ matrix.target }} yarn build
|
||||
- run: npm install
|
||||
- run: npm run zip -- -b ${{ matrix.target }}
|
||||
|
||||
- name: Drop build artifacts (${{ matrix.target }})
|
||||
uses: actions/upload-artifact@main
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: dist
|
||||
path: ./.output/password-generator-*-${{ matrix.target }}.zip
|
||||
include-hidden-files: true
|
||||
|
||||
- name: web-ext lint
|
||||
if: ${{ matrix.target == 'firefox' }}
|
||||
uses: freaktechnik/web-ext-lint@main
|
||||
with:
|
||||
extension-root: dist
|
||||
extension-root: ./.output/firefox-mv3
|
||||
self-hosted: false
|
||||
|
||||
- uses: cardinalby/webext-buildtools-pack-extension-dir-action@1.0.9
|
||||
with:
|
||||
extensionDir: dist
|
||||
zipFilePath: PasswordGenerator-${{ matrix.target }}.zip
|
||||
|
||||
- name: Drop packed artifacts (${{ matrix.target }})
|
||||
uses: actions/upload-artifact@main
|
||||
with:
|
||||
name: packed-${{ matrix.target }}
|
||||
path: PasswordGenerator-${{ matrix.target }}.zip
|
||||
- run: npm audit
|
||||
if: ${{ github.event_name == 'release' || github.event.inputs.bypass_audit == 'false' }}
|
||||
|
||||
publish-github:
|
||||
needs: build
|
||||
@@ -83,14 +79,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/download-artifact@main
|
||||
with:
|
||||
name: packed-${{ matrix.target }}
|
||||
name: ${{ matrix.target }}
|
||||
|
||||
- name: Attach build to release
|
||||
uses: xresloader/upload-to-github-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
file: PasswordGenerator-${{ matrix.target }}.zip
|
||||
file: password-generator-*-${{ matrix.target }}.zip
|
||||
draft: false
|
||||
overwrite: true
|
||||
update_latest_release: true
|
||||
@@ -103,12 +99,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/download-artifact@main
|
||||
with:
|
||||
name: packed-chrome
|
||||
name: chrome
|
||||
|
||||
- uses: wdzeng/chrome-extension@v1.2.4
|
||||
- uses: wdzeng/chrome-extension@v1.3.0
|
||||
with:
|
||||
extension-id: jnjobgjobffgmgfnkpkjfjkkfhfikmfl
|
||||
zip-path: PasswordGenerator-chrome.zip
|
||||
zip-path: password-generator-*-chrome.zip
|
||||
client-id: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
@@ -121,15 +117,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/download-artifact@main
|
||||
with:
|
||||
name: packed-chrome
|
||||
name: chrome
|
||||
|
||||
- uses: wdzeng/edge-addon@v1.2.4
|
||||
- uses: wdzeng/edge-addon@v2.1.0
|
||||
with:
|
||||
product-id: ${{ secrets.EDGE_PRODUCT_ID }}
|
||||
zip-path: PasswordGenerator-chrome.zip
|
||||
zip-path: password-generator-*-chrome.zip
|
||||
client-id: ${{ secrets.EDGE_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.EDGE_CLIENT_SECRET }}
|
||||
access-token-url: ${{ secrets.EDGE_ACCESS_TOKEN_URL }}
|
||||
api-key: ${{ secrets.EDGE_API_KEY }}
|
||||
|
||||
publish-firefox:
|
||||
needs: build
|
||||
@@ -139,11 +134,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/download-artifact@main
|
||||
with:
|
||||
name: packed-firefox
|
||||
name: firefox
|
||||
|
||||
- uses: wdzeng/firefox-addon@v1.0.5
|
||||
- uses: wdzeng/firefox-addon@v1.2.0
|
||||
with:
|
||||
addon-guid: ${{ secrets.FIREFOX_EXT_UUID }}
|
||||
xpi-path: PasswordGenerator-firefox.zip
|
||||
xpi-path: password-generator-*-firefox.zip
|
||||
jwt-issuer: ${{ secrets.FIREFOX_API_KEY }}
|
||||
jwt-secret: ${{ secrets.FIREFOX_CLIENT_SECRET }}
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
extver=`jq -r ".version" package.json`
|
||||
echo "version=$extver" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: dev-build-deploy/release-me@v0.16.3
|
||||
- uses: dev-build-deploy/release-me@v0.18.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: v
|
||||
|
||||
@@ -37,19 +37,21 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
|
||||
- run: yarn install
|
||||
- run: yarn lint
|
||||
- run: TARGET=${{ matrix.target }} yarn build
|
||||
- run: npm install
|
||||
- run: npm run zip -- -b ${{ matrix.target }}
|
||||
|
||||
- name: Drop artifacts (${{ matrix.target }})
|
||||
uses: actions/upload-artifact@main
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: dist
|
||||
path: ./.output/password-generator-*-${{ matrix.target }}.zip
|
||||
include-hidden-files: true
|
||||
|
||||
- name: web-ext lint
|
||||
if: ${{ matrix.target == 'firefox' }}
|
||||
uses: freaktechnik/web-ext-lint@main
|
||||
with:
|
||||
extension-root: dist
|
||||
extension-root: ./.output/firefox-mv3
|
||||
self-hosted: false
|
||||
|
||||
- run: npm audit
|
||||
|
||||
@@ -8,9 +8,11 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
@@ -20,7 +22,3 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Config files
|
||||
.webextrc
|
||||
.webextrc.*
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"DNEK.auto-region-folder",
|
||||
"eamodio.gitlens",
|
||||
"jock.svg",
|
||||
"firefox-devtools.vscode-firefox-debug",
|
||||
"bierner.github-markdown-preview",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"github.vscode-github-actions",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"bierner.github-markdown-preview",
|
||||
"mrmlnc.vscode-scss",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"redhat.vscode-yaml"
|
||||
"jock.svg",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"saeris.markdown-github-alerts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"/messages.json"
|
||||
],
|
||||
"url": "https://gist.github.com/XFox111/9528b76f9f02704d620d4edbf421e06b/raw/e77197276f0aa2994cceae4ddf4dcfcabdce9dcb/webext-locale-schema.json"
|
||||
}
|
||||
],
|
||||
"editor.rulers": [
|
||||
{
|
||||
"column": 120
|
||||
@@ -29,14 +21,6 @@
|
||||
"css.lint.float": "warning",
|
||||
"css.lint.unknownVendorSpecificProperties": "warning",
|
||||
"css.lint.zeroUnits": "warning",
|
||||
"scss.format.braceStyle": "expand",
|
||||
"scss.format.maxPreserveNewLines": 3,
|
||||
"scss.format.spaceAroundSelectorSeparator": true,
|
||||
"scss.lint.compatibleVendorPrefixes": "warning",
|
||||
"scss.lint.duplicateProperties": "warning",
|
||||
"scss.lint.float": "warning",
|
||||
"scss.lint.unknownVendorSpecificProperties": "warning",
|
||||
"scss.lint.zeroUnits": "warning",
|
||||
"html.format.maxPreserveNewLines": 3,
|
||||
"html.format.wrapAttributes": "preserve",
|
||||
"javascript.format.placeOpenBraceOnNewLineForControlBlocks": true,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn build",
|
||||
"type": "npm",
|
||||
"script": "build -- -b chrome",
|
||||
"group":
|
||||
{
|
||||
"kind": "build",
|
||||
@@ -12,67 +12,32 @@
|
||||
"label": "Build: Chromium"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn build",
|
||||
"group":
|
||||
{
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
},
|
||||
"label": "Build: Firefox",
|
||||
"options": {
|
||||
"env": {
|
||||
"TARGET": "firefox"
|
||||
}
|
||||
}
|
||||
"type": "npm",
|
||||
"script": "build -- -b firefox",
|
||||
"group": "build",
|
||||
"label": "Build: Firefox"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn build",
|
||||
"args": [
|
||||
"--watch",
|
||||
"--mode",
|
||||
"development"
|
||||
],
|
||||
"type": "npm",
|
||||
"script": "dev -- -b chrome",
|
||||
"group": "test",
|
||||
"label": "Watch: Chromium"
|
||||
"label": "Dev: Google Chrome"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn build",
|
||||
"args": [
|
||||
"--watch",
|
||||
"--mode",
|
||||
"development"
|
||||
],
|
||||
"type": "npm",
|
||||
"script": "dev -- -b firefox",
|
||||
"group": "test",
|
||||
"label": "Watch: Firefox",
|
||||
"options": {
|
||||
"env": {
|
||||
"TARGET": "firefox"
|
||||
}
|
||||
}
|
||||
"label": "Dev: Firefox"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn dev",
|
||||
"type": "npm",
|
||||
"script": "dev -- -b edge",
|
||||
"group": "test",
|
||||
"label": "Dev: Chromium"
|
||||
"label": "Dev: Microsoft Edge"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn dev",
|
||||
"group": "test",
|
||||
"label": "Dev: Firefox",
|
||||
"options": {
|
||||
"env": {
|
||||
"TARGET": "firefox"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn install",
|
||||
"type": "npm",
|
||||
"script": "install",
|
||||
"label": "Restore dependencies",
|
||||
"group": "build"
|
||||
}
|
||||
|
||||
@@ -2,75 +2,133 @@
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at opensource@xfox111.net. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[opensource@xfox111.net](mailto:opensource@xfox111.net).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
> Contributor Covenant is released under the [Creative Commons Attribution 4.0 International Public License](https://github.com/EthicalSource/contributor_covenant/blob/release/LICENSE.md).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
# Contribution Guidelines
|
||||
This article has been moved to the [project's Wiki section](https://github.com/XFox111/PasswordGeneratorExtension/wiki/Contribution-Guidelines)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This article has been moved to the [project's Wiki section](https://github.com/XFox111/PasswordGeneratorExtension/wiki/Contribution-Guidelines)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -9,20 +9,22 @@
|
||||
<img alt="Password generator">
|
||||
</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
|
||||
- Create strong passwords in one click
|
||||
- Customizable generator
|
||||
- Clean and simple UI
|
||||
- Dark mode
|
||||
- **NEW:** Advanced password generator
|
||||
- **NEW:** Passphrase generator
|
||||
|
||||
## Languages
|
||||
- Chinese (Simplified)
|
||||
- English
|
||||
- Ukrainian
|
||||
- Polish
|
||||
- Russian
|
||||
- Portuguese (Brazil)
|
||||
- Russian
|
||||
- Ukrainian
|
||||
|
||||
## Download
|
||||
[](https://chrome.google.com/webstore/detail/jnjobgjobffgmgfnkpkjfjkkfhfikmfl)
|
||||
@@ -45,7 +47,7 @@ Extension for web browsers which helps you to easily generate strong passwords i
|
||||
|
||||
> 1. Go to [Releases](https://github.com/XFox111/PasswordGeneratorExtension/releases) and select a release to download
|
||||
> 2. Download attached archive for Chromium and unpack it
|
||||
> 3. Go to "chrome://extensions"
|
||||
> 3. Go to `chrome://extensions`
|
||||
> 4. Enable "Developer mode"
|
||||
> 5. Click the "Load unpacked" button and navigate to the extension's root folder (contains `manifest.json`)
|
||||
> 6. Done!
|
||||
@@ -57,8 +59,8 @@ Extension for web browsers which helps you to easily generate strong passwords i
|
||||
|
||||
> 1. Go to [Releases](https://github.com/XFox111/PasswordGeneratorExtension/releases) and select a release to download
|
||||
> 2. Download attached archive for Firefox and unpack it
|
||||
> 3. Go to "about:debugging#/runtime/this-firefox"
|
||||
> 4. Click the "Load Temporary Add-on..." button and select `manifest.josn` file in the root folder
|
||||
> 3. Go to `about:debugging#/runtime/this-firefox`
|
||||
> 4. Click the "Load Temporary Add-on..." button and select `manifest.json` file in the root folder
|
||||
> 5. Done!
|
||||
|
||||
> **Important!**
|
||||
@@ -86,8 +88,8 @@ If you are interested in fixing issues and contributing directly to the code bas
|
||||
|
||||
---
|
||||
|
||||
[](https://twitter.com/xfox111)
|
||||
[](https://github.com/xfox111)
|
||||
[](https://buymeacoffee.com/xfox111)
|
||||
[](https://bsky.app/profile/xfox111.net)
|
||||
[](https://github.com/xfox111)
|
||||
[](https://buymeacoffee.com/xfox111)
|
||||
|
||||
> ©2024 Eugene Fox
|
||||
> ©2025 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/PasswordGeneratorExtension/blob/main/LICENSE)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
height: "100vh",
|
||||
gap: tokens.spacingHorizontalXL,
|
||||
padding: `${tokens.spacingVerticalXL} ${tokens.spacingHorizontalXL}`,
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
smallRoot:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column-reverse",
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalS}`,
|
||||
},
|
||||
hideScroll:
|
||||
{
|
||||
overflowY: "visible",
|
||||
maxHeight: "unset",
|
||||
},
|
||||
configRoot:
|
||||
{
|
||||
maxHeight: "90vh",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalM,
|
||||
width: "100%",
|
||||
maxWidth: "480px",
|
||||
borderRadius: tokens.borderRadiusLarge,
|
||||
boxShadow: tokens.shadow4,
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
|
||||
margin: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
|
||||
boxSizing: "border-box",
|
||||
justifySelf: "center",
|
||||
alignSelf: "center",
|
||||
overflowY: "auto",
|
||||
},
|
||||
switch:
|
||||
{
|
||||
margin: `${tokens.spacingVerticalXL} ${tokens.spacingVerticalNone}`,
|
||||
},
|
||||
listRoot:
|
||||
{
|
||||
width: "100%",
|
||||
maxWidth: "840px",
|
||||
alignSelf: "center",
|
||||
justifySelf: "center",
|
||||
padding: `${tokens.spacingVerticalXL} ${tokens.spacingHorizontalXL}`,
|
||||
boxSizing: "border-box",
|
||||
overflowY: "auto",
|
||||
maxHeight: "100%",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import DoubleLabledSwitch from "@/shared/DoubleLabeledSwitch";
|
||||
import { mergeClasses, Toaster } from "@fluentui/react-components";
|
||||
import { ReactElement } from "react";
|
||||
import { useMediaQuery } from "react-responsive";
|
||||
import PasswordList from "./components/PasswordList";
|
||||
import { useStyles } from "./Page.styles";
|
||||
import PassphraseSection from "./sections/PassphraseSection";
|
||||
import PasswordSection from "./sections/PasswordSection";
|
||||
|
||||
export default function Page(): ReactElement
|
||||
{
|
||||
const [isPassphrase, setIsPassphrase] = useState<boolean | null>(null);
|
||||
const [passwords, setPasswords] = useState<string[]>([]);
|
||||
const isSmall = useMediaQuery({ query: "(max-width: 1000px)" });
|
||||
|
||||
const cls = useStyles();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
advancedPassphraseSelected.getValue().then(setIsPassphrase);
|
||||
const unwatch = advancedPassphraseSelected.watch(setIsPassphrase);
|
||||
|
||||
return () => unwatch();
|
||||
}, []);
|
||||
|
||||
if (isPassphrase === null)
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<main className={ mergeClasses(cls.root, isSmall && cls.smallRoot) }>
|
||||
<PasswordList
|
||||
passwords={ passwords }
|
||||
className={ mergeClasses(cls.listRoot, isSmall && cls.hideScroll) } />
|
||||
|
||||
<article className={ mergeClasses(cls.configRoot, isSmall && cls.hideScroll) }>
|
||||
<DoubleLabledSwitch outerRoot={ { className: cls.switch } }
|
||||
checked={ isPassphrase }
|
||||
onChange={ (_, e) => advancedPassphraseSelected.setValue(e.checked) }
|
||||
offLabel={ i18n.t("advanced.password.title") }
|
||||
onLabel={ i18n.t("advanced.passphrase.title") } />
|
||||
|
||||
{ isPassphrase ?
|
||||
<PassphraseSection onGenerated={ setPasswords } />
|
||||
:
|
||||
<PasswordSection onGenerated={ setPasswords } />
|
||||
}
|
||||
</article>
|
||||
<Toaster />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export type GeneratorProps =
|
||||
{
|
||||
onGenerated: (passwords: string[]) => void;
|
||||
};
|
||||
|
||||
const advancedPassphraseSelected = storage.defineItem<boolean>("sync:AdvancedPassphraseSelected", { fallback: false });
|
||||
@@ -0,0 +1,22 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalXL,
|
||||
},
|
||||
actionRoot:
|
||||
{
|
||||
display: "flex",
|
||||
gap: tokens.spacingHorizontalM,
|
||||
alignSelf: "center",
|
||||
},
|
||||
bulkRoot:
|
||||
{
|
||||
display: "grid",
|
||||
alignItems: "center",
|
||||
gridTemplateColumns: "24px 24px 56px",
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Button, Input, InputOnChangeData, MessageBar, MessageBarBody, MessageBarTitle, Text, Toast, ToastTitle, useToastController } from "@fluentui/react-components";
|
||||
import { bundleIcon, Key24Regular, Save20Filled, Save20Regular } from "@fluentui/react-icons";
|
||||
import { PropsWithChildren, ReactElement } from "react";
|
||||
import { useStyles } from "./GeneratorForm.styles";
|
||||
|
||||
export default function GeneratorForm(props: GeneratorFormProps): ReactElement
|
||||
{
|
||||
const [passwordCount, private_setPasswordCount] = useState<number | null>(5);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const toaster = useToastController();
|
||||
|
||||
const cls = useStyles();
|
||||
const SaveIcon = bundleIcon(Save20Filled, Save20Regular);
|
||||
|
||||
const setPasswordCount = useCallback((_: any, e: InputOnChangeData) =>
|
||||
{
|
||||
const n = parseInt(e.value ?? "1");
|
||||
private_setPasswordCount(isNaN(n) || n < 1 ? null : Math.min(n, 1000));
|
||||
}, []);
|
||||
|
||||
const onSubmit = useCallback((args: React.FormEvent<HTMLFormElement>) =>
|
||||
{
|
||||
args.preventDefault();
|
||||
|
||||
try
|
||||
{
|
||||
setError(null);
|
||||
props.onGenerate(passwordCount ?? 1);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
setError((ex as Error).message);
|
||||
}
|
||||
}, [props.onGenerate, passwordCount]);
|
||||
|
||||
const onSave = useCallback(async () =>
|
||||
{
|
||||
props.onSave();
|
||||
await browser.storage.sync.set({ AdvancedBulkCount: passwordCount ?? 5 });
|
||||
|
||||
toaster.dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>{ i18n.t("advanced.saved_msg") }</ToastTitle>
|
||||
</Toast>,
|
||||
{
|
||||
intent: "success",
|
||||
timeout: 1000
|
||||
}
|
||||
);
|
||||
}, [props.onSave, toaster, passwordCount]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
browser.storage.sync.get("AdvancedBulkCount").then(({ AdvancedBulkCount }) =>
|
||||
private_setPasswordCount(AdvancedBulkCount as number ?? 5)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form onSubmit={ onSubmit } className={ cls.root }>
|
||||
{ props.children }
|
||||
|
||||
{ error &&
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>{ error }</MessageBarTitle>
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
}
|
||||
|
||||
<div className={ cls.actionRoot }>
|
||||
<div className={ cls.bulkRoot }>
|
||||
<Key24Regular />
|
||||
<Text align="center">x</Text>
|
||||
<Input value={ passwordCount?.toString() ?? "" } onChange={ setPasswordCount } />
|
||||
</div>
|
||||
<Button appearance="primary" type="submit">{ i18n.t("advanced.actions.generate") }</Button>
|
||||
</div>
|
||||
|
||||
<Button appearance="subtle" icon={ <SaveIcon /> } onClick={ onSave }>
|
||||
{ i18n.t("advanced.actions.save_config") }
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export type GeneratorFormProps = PropsWithChildren &
|
||||
{
|
||||
onSave: () => void;
|
||||
onGenerate: (count: number) => void;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalS,
|
||||
},
|
||||
copyAll:
|
||||
{
|
||||
alignSelf: "flex-end",
|
||||
minHeight: "32px",
|
||||
},
|
||||
table:
|
||||
{
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
borderRadius: "16px",
|
||||
overflow: "clip",
|
||||
},
|
||||
row:
|
||||
{
|
||||
"&:last-child":
|
||||
{
|
||||
borderBottom: "none",
|
||||
}
|
||||
},
|
||||
cell:
|
||||
{
|
||||
padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
|
||||
cursor: "pointer",
|
||||
},
|
||||
cellLayout:
|
||||
{
|
||||
overflowX: "auto",
|
||||
},
|
||||
passwordText:
|
||||
{
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
overflowX: "hidden",
|
||||
display: "block",
|
||||
marginRight: "36px",
|
||||
},
|
||||
copyIcon:
|
||||
{
|
||||
verticalAlign: "middle",
|
||||
padding: tokens.spacingHorizontalXL,
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { bundleIcon, Copy32Regular, CopyFilled, CopyRegular } from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import { useStyles } from "./PasswordList.styles";
|
||||
|
||||
export default function PasswordList({ passwords, className }: PasswordListProps): ReactElement
|
||||
{
|
||||
const toaster = fui.useToastController();
|
||||
|
||||
const CopyIcon = bundleIcon(CopyFilled, CopyRegular);
|
||||
const cls = useStyles();
|
||||
|
||||
const copy = (password?: string) =>
|
||||
{
|
||||
navigator.clipboard.writeText(password ?? passwords.join("\n"));
|
||||
toaster.dispatchToast(
|
||||
<fui.Toast>
|
||||
<fui.ToastTitle>{ i18n.t("advanced.copied_msg") }</fui.ToastTitle>
|
||||
</fui.Toast>,
|
||||
{
|
||||
intent: "success",
|
||||
timeout: 1000
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ fui.mergeClasses(cls.root, className) }>
|
||||
{ passwords.length > 0 &&
|
||||
<>
|
||||
<fui.Button className={ cls.copyAll }
|
||||
appearance="subtle" icon={ <CopyIcon /> }
|
||||
onClick={ () => copy() }>
|
||||
|
||||
{ i18n.t("advanced.actions.copy_all") }
|
||||
</fui.Button>
|
||||
|
||||
<fui.Table className={ cls.table }>
|
||||
<fui.TableBody>
|
||||
|
||||
{ passwords.map((password, index) =>
|
||||
<fui.TableRow key={ index } className={ cls.row }
|
||||
onClick={ () => copy(password) }>
|
||||
|
||||
<fui.TableCell className={ cls.cell }>
|
||||
|
||||
<fui.TableCellLayout content={ { className: cls.cellLayout } }>
|
||||
<fui.Tooltip relationship="description" content={ password }>
|
||||
|
||||
<fui.Text className={ cls.passwordText }
|
||||
font="monospace" size={ 600 }>
|
||||
|
||||
{ password }
|
||||
</fui.Text>
|
||||
|
||||
</fui.Tooltip>
|
||||
</fui.TableCellLayout>
|
||||
|
||||
<fui.TableCellActions>
|
||||
<Copy32Regular className={ cls.copyIcon } />
|
||||
</fui.TableCellActions>
|
||||
|
||||
</fui.TableCell>
|
||||
</fui.TableRow>
|
||||
) }
|
||||
|
||||
</fui.TableBody>
|
||||
</fui.Table>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type PasswordListProps =
|
||||
{
|
||||
className?: string;
|
||||
passwords: string[];
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Advanced password generator</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,51 @@
|
||||
html,
|
||||
body,
|
||||
#root > .fui-FluentProvider
|
||||
{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
p, h1, h2
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar
|
||||
{
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track
|
||||
{
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb
|
||||
{
|
||||
border-radius: 4px;
|
||||
background: light-dark(#d1d1d1, #666666);
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover
|
||||
{
|
||||
background: light-dark(#c7c7c7, #757575);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover:active
|
||||
{
|
||||
background: light-dark(#b3b3b3, #6b6b6b);
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./popup.css";
|
||||
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 />
|
||||
<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;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalM,
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
|
||||
},
|
||||
horizontalContainer:
|
||||
{
|
||||
display: "flex",
|
||||
gap: tokens.spacingHorizontalSNudge,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { bmcDarkTheme, bmcLightTheme } from "@/utils/BmcTheme";
|
||||
import { getFeedbackLink, githubLinks, personalLinks } from "@/utils/links";
|
||||
import { useTheme } from "@/utils/useTheme";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { PersonFeedbackRegular } from "@fluentui/react-icons";
|
||||
import { ReactElement, ReactNode } from "react";
|
||||
import { useStyles } from "./AboutSection.styles";
|
||||
|
||||
export default function AboutSection(): ReactElement
|
||||
{
|
||||
const bmcTheme = useTheme(bmcLightTheme, bmcDarkTheme);
|
||||
const cls = useStyles();
|
||||
|
||||
return (
|
||||
<section className={ cls.root }>
|
||||
<header className={ cls.horizontalContainer }>
|
||||
<fui.Subtitle1 as="h1">{ i18n.t("manifest.name") }</fui.Subtitle1>
|
||||
<fui.Caption1 as="span">v{ browser.runtime.getManifest().version }</fui.Caption1>
|
||||
</header>
|
||||
|
||||
<fui.Text as="p">
|
||||
{ i18n.t("about.developed_by") } ({ link("@xfox111.net", personalLinks.social) })
|
||||
<br />
|
||||
{ i18n.t("about.licensed_under") } { link(i18n.t("about.mit_license"), githubLinks.license) }
|
||||
</fui.Text>
|
||||
|
||||
<fui.Text as="p">
|
||||
{ i18n.t("about.translation_cta.text") }<br />
|
||||
{ link(i18n.t("about.translation_cta.button"), githubLinks.translationGuide) }
|
||||
</fui.Text>
|
||||
|
||||
<fui.Text as="p">
|
||||
{ link(i18n.t("about.links.website"), personalLinks.website) } <br />
|
||||
{ link(i18n.t("about.links.source"), githubLinks.repository) } <br />
|
||||
{ link(i18n.t("about.links.changelog"), githubLinks.changelog) }
|
||||
</fui.Text>
|
||||
|
||||
<div className={ cls.horizontalContainer }>
|
||||
<fui.Button { ...buttonProps(getFeedbackLink(), <PersonFeedbackRegular />) }>
|
||||
{ i18n.t("about.cta.feedback") }
|
||||
</fui.Button>
|
||||
<fui.FluentProvider theme={ bmcTheme }>
|
||||
<fui.Button { ...buttonProps(personalLinks.donation, <img style={ { height: 20 } } src="bmc.svg" />) }>
|
||||
{ i18n.t("about.cta.sponsor") }
|
||||
</fui.Button>
|
||||
</fui.FluentProvider>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const link = (text: string, href: string): ReactNode => (
|
||||
<fui.Link target="_blank" href={ href }>{ text }</fui.Link>
|
||||
);
|
||||
|
||||
const buttonProps = (href: string, icon: JSX.Element): fui.ButtonProps => (
|
||||
{
|
||||
as: "a", target: "_blank", href,
|
||||
appearance: "primary", icon
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalMNudge,
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalMNudge}`,
|
||||
},
|
||||
checkboxContainer:
|
||||
{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
rangeContainer:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto 1fr auto",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingHorizontalS,
|
||||
},
|
||||
rangeInput:
|
||||
{
|
||||
width: "100%",
|
||||
},
|
||||
excludeCustom:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "auto 1fr",
|
||||
gap: tokens.spacingHorizontalS,
|
||||
alignItems: "center",
|
||||
},
|
||||
divider:
|
||||
{
|
||||
flexGrow: 0,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { CharacterHints } from "@/utils/generators/generatePassword";
|
||||
import { ExtensionOptions, GeneratorOptions, useStorage } from "@/utils/storage";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { ArrowUndoRegular } from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import infoLabel from "../../utils/infoLabel";
|
||||
import { useStyles } from "./SettingsSection.styles";
|
||||
|
||||
export default function SettingsSection(): ReactElement
|
||||
{
|
||||
const { extOptions, generatorOptions, updateStorage } = useStorage();
|
||||
const cls = useStyles();
|
||||
|
||||
const resetRange = useCallback(() =>
|
||||
{
|
||||
updateStorage({
|
||||
MinLength: defaultOptions.extension.MinLength,
|
||||
MaxLength: defaultOptions.extension.MaxLength
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setOption = (option: keyof (GeneratorOptions & ExtensionOptions)) =>
|
||||
(_: unknown, args: fui.CheckboxOnChangeData) =>
|
||||
updateStorage({ [option]: args.checked });
|
||||
|
||||
const updateNumberField = (key: keyof (ExtensionOptions & GeneratorOptions), defaultValue: number) =>
|
||||
(_: unknown, e: fui.InputOnChangeData): void =>
|
||||
{
|
||||
if (e.value.length >= 1)
|
||||
{
|
||||
const value = parseInt(e.value);
|
||||
|
||||
if (!isNaN(value) && value >= 0)
|
||||
updateStorage({ [key]: value });
|
||||
}
|
||||
else
|
||||
updateStorage({ [key]: defaultValue });
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={ cls.root }>
|
||||
|
||||
<fui.Field label={ i18n.t("settings.length.title") } hint={ i18n.t("settings.length.hint") }>
|
||||
<fui.Input
|
||||
value={ generatorOptions.Length.toString() }
|
||||
onChange={ updateNumberField("Length", 0) } />
|
||||
</fui.Field>
|
||||
|
||||
<fui.Field label={ i18n.t("settings.quick_range") }>
|
||||
<div className={ cls.rangeContainer }>
|
||||
<fui.Input
|
||||
input={ { className: cls.rangeInput } }
|
||||
value={ extOptions.MinLength.toString() }
|
||||
onChange={ updateNumberField("MinLength", defaultOptions.extension.MinLength) } />
|
||||
|
||||
<fui.Divider />
|
||||
|
||||
<fui.Input
|
||||
input={ { className: cls.rangeInput } }
|
||||
value={ extOptions.MaxLength.toString() }
|
||||
onChange={ updateNumberField("MaxLength", defaultOptions.extension.MaxLength) } />
|
||||
|
||||
<fui.Tooltip relationship="label" content={ i18n.t("common.actions.reset") }>
|
||||
<fui.Button
|
||||
appearance="subtle" icon={ <ArrowUndoRegular /> }
|
||||
onClick={ resetRange } />
|
||||
</fui.Tooltip>
|
||||
</div>
|
||||
</fui.Field>
|
||||
|
||||
<fui.Divider className={ cls.divider } />
|
||||
|
||||
<fui.Text>{ i18n.t("common.sections.include") }</fui.Text>
|
||||
<div className={ cls.checkboxContainer }>
|
||||
<fui.Checkbox label={ i18n.t("common.characters.uppercase") }
|
||||
checked={ generatorOptions.Uppercase }
|
||||
onChange={ setOption("Uppercase") } />
|
||||
<fui.Checkbox
|
||||
label={ i18n.t("common.characters.lowercase") }
|
||||
checked={ generatorOptions.Lowercase }
|
||||
onChange={ setOption("Lowercase") } />
|
||||
<fui.Checkbox
|
||||
label={ i18n.t("common.characters.numeric") }
|
||||
checked={ generatorOptions.Numeric }
|
||||
onChange={ setOption("Numeric") } />
|
||||
<fui.Checkbox
|
||||
label={ infoLabel(i18n.t("common.characters.special"), CharacterHints.special) }
|
||||
checked={ generatorOptions.Special }
|
||||
onChange={ setOption("Special") } />
|
||||
<div>
|
||||
<fui.Checkbox checked={ generatorOptions.Custom } onChange={ setOption("Custom") } />
|
||||
<fui.Input size="small" placeholder={ i18n.t("common.characters.custom") }
|
||||
value={ generatorOptions.IncludeCustomSet }
|
||||
onChange={ (_, e) => updateStorage({ IncludeCustomSet: e.value }) } />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fui.Text>{ i18n.t("common.sections.exclude") }</fui.Text>
|
||||
<div className={ cls.checkboxContainer }>
|
||||
<fui.Checkbox
|
||||
label={ infoLabel(i18n.t("common.characters.similar"), CharacterHints.similar) }
|
||||
checked={ generatorOptions.ExcludeSimilar }
|
||||
onChange={ setOption("ExcludeSimilar") } />
|
||||
<fui.Checkbox
|
||||
label={ infoLabel(i18n.t("common.characters.ambiguous"), CharacterHints.ambiguous) }
|
||||
disabled={ !generatorOptions.Special }
|
||||
checked={ generatorOptions.ExcludeAmbiguous }
|
||||
onChange={ setOption("ExcludeAmbiguous") } />
|
||||
<fui.Checkbox
|
||||
label={ infoLabel(i18n.t("common.characters.repeating.label"), i18n.t("common.characters.repeating.hint")) }
|
||||
checked={ generatorOptions.ExcludeRepeating }
|
||||
onChange={ setOption("ExcludeRepeating") } />
|
||||
<div>
|
||||
<fui.Checkbox checked={ generatorOptions.ExcludeCustom } onChange={ setOption("ExcludeCustom") } />
|
||||
<fui.Input size="small" placeholder={ i18n.t("common.characters.custom") }
|
||||
value={ generatorOptions.ExcludeCustomSet }
|
||||
onChange={ (_, e) => updateStorage({ ExcludeCustomSet: e.value }) } />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultOptions =
|
||||
{
|
||||
generator: new GeneratorOptions(),
|
||||
extension: new ExtensionOptions()
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Settings - Password generator</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
html,
|
||||
body,
|
||||
#root > .fui-FluentProvider
|
||||
{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 450px;
|
||||
|
||||
overflow: auto;
|
||||
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
main
|
||||
{
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
section
|
||||
{
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
p, h1, h2
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar
|
||||
{
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track
|
||||
{
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb
|
||||
{
|
||||
border-radius: 4px;
|
||||
background: light-dark(#d1d1d1, #666666);
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover
|
||||
{
|
||||
background: light-dark(#c7c7c7, #757575);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover:active
|
||||
{
|
||||
background: light-dark(#b3b3b3, #6b6b6b);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Tab, TabList } from "@fluentui/react-components";
|
||||
import { bundleIcon, FluentIcon, Info20Filled, Info20Regular, Settings20Filled, Settings20Regular } from "@fluentui/react-icons";
|
||||
import { ReactElement, StrictMode } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "../../shared/App";
|
||||
import AboutSection from "./AboutSection";
|
||||
import "./main.css";
|
||||
import SettingsSection from "./SettingsSection";
|
||||
|
||||
function Options(): ReactElement
|
||||
{
|
||||
const [selection, setSelection] = useState<string>("settings");
|
||||
|
||||
const SettingsIcon: FluentIcon = bundleIcon(Settings20Filled, Settings20Regular);
|
||||
const AboutIcon: FluentIcon = bundleIcon(Info20Filled, Info20Regular);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<TabList selectedValue={ selection } onTabSelect={ (_, e) => setSelection(e.value as string) }>
|
||||
<Tab icon={ <SettingsIcon /> } content={ i18n.t("settings.title") } value="settings" />
|
||||
<Tab icon={ <AboutIcon /> } content={ i18n.t("about.title") } value="about" />
|
||||
</TabList>
|
||||
{ selection === "settings" && <SettingsSection /> }
|
||||
{ selection === "about" && <AboutSection /> }
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App>
|
||||
<Options />
|
||||
</App>
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
input:
|
||||
{
|
||||
fontFamily: tokens.fontFamilyMonospace,
|
||||
},
|
||||
copyIcon:
|
||||
{
|
||||
animationName: "scaleUpIn",
|
||||
animationDuration: tokens.durationSlow,
|
||||
animationTimingFunction: tokens.curveEasyEaseMax,
|
||||
},
|
||||
refreshIcon:
|
||||
{
|
||||
animationName: "spin",
|
||||
animationDuration: tokens.durationSlow,
|
||||
animationTimingFunction: tokens.curveEasyEaseMax,
|
||||
},
|
||||
msgBar:
|
||||
{
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
|
||||
},
|
||||
msgBarBody:
|
||||
{
|
||||
whiteSpace: "break-spaces",
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { generatePassword } from "@/utils/generators/generatePassword";
|
||||
import { GeneratorOptions } from "@/utils/storage";
|
||||
import useTimeout from "@/utils/useTimeout";
|
||||
import { Button, Input, mergeClasses, MessageBar, MessageBarBody, Tooltip } from "@fluentui/react-components";
|
||||
import { ArrowClockwise20Regular, CheckmarkRegular, Copy20Regular } from "@fluentui/react-icons";
|
||||
import { ReactElement, useEffect, useState } from "react";
|
||||
import { useStyles } from "./GeneratorView.styles";
|
||||
|
||||
export default function GeneratorView({ options }: GeneratorViewProps): ReactElement
|
||||
{
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [refreshTimer, copyTimer] = [useTimeout(310), useTimeout(1000)];
|
||||
const cls = useStyles();
|
||||
|
||||
const refresh = useCallback(() =>
|
||||
{
|
||||
if (!options)
|
||||
return;
|
||||
|
||||
setError(null);
|
||||
try
|
||||
{
|
||||
setPassword(generatePassword({
|
||||
length: options.Length,
|
||||
uppercase: options.Uppercase,
|
||||
lowercase: options.Lowercase,
|
||||
numeric: options.Numeric,
|
||||
special: options.Special,
|
||||
custom: options.Custom,
|
||||
excludeSimilar: options.ExcludeSimilar,
|
||||
excludeAmbiguous: options.ExcludeAmbiguous,
|
||||
excludeRepeating: options.ExcludeRepeating,
|
||||
excludeCustom: options.ExcludeCustom ? options.ExcludeCustomSet : "",
|
||||
customSet: options.IncludeCustomSet
|
||||
}));
|
||||
}
|
||||
catch (e) { setError((e as Error).message); }
|
||||
|
||||
refreshTimer.trigger();
|
||||
}, [options]);
|
||||
|
||||
const copy = useCallback(async () =>
|
||||
{
|
||||
await window.navigator.clipboard.writeText(password);
|
||||
copyTimer.trigger();
|
||||
}, [password]);
|
||||
|
||||
useEffect(refresh, [options]);
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<MessageBar intent="warning" className={ cls.msgBar }>
|
||||
<MessageBarBody className={ cls.msgBarBody }>{ error }</MessageBarBody>
|
||||
</MessageBar>
|
||||
);
|
||||
|
||||
return (
|
||||
<Input
|
||||
className={ cls.input }
|
||||
readOnly value={ password }
|
||||
contentAfter={ <>
|
||||
<Tooltip content={ i18n.t("common.actions.copy") } relationship="label">
|
||||
<Button
|
||||
appearance="subtle" onClick={ copy }
|
||||
icon={
|
||||
copyTimer.isActive ?
|
||||
<CheckmarkRegular className={ cls.copyIcon } /> :
|
||||
<Copy20Regular className={ cls.copyIcon } />
|
||||
} />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={ i18n.t("popup.refresh") } relationship="label">
|
||||
<Button
|
||||
appearance="subtle" onClick={ refresh }
|
||||
icon={
|
||||
<ArrowClockwise20Regular className={ mergeClasses(refreshTimer.isActive && cls.refreshIcon) } />
|
||||
} />
|
||||
</Tooltip>
|
||||
</> } />
|
||||
);
|
||||
};
|
||||
|
||||
export type GeneratorViewProps =
|
||||
{
|
||||
options: GeneratorOptions | null;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
characterOptionsContainer:
|
||||
{
|
||||
display: "flex",
|
||||
gap: tokens.spacingHorizontalXS,
|
||||
},
|
||||
options:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalS,
|
||||
},
|
||||
lengthContainer:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
paddingRight: tokens.spacingHorizontalM,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import { GeneratorOptions, useStorage } from "@/utils/storage";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import * as ic from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import { useStyles } from "./QuickOptions.styles";
|
||||
|
||||
export default function QuickOptions({ onChange }: QuickOptionsProps): ReactElement
|
||||
{
|
||||
const { extOptions, generatorOptions } = useStorage();
|
||||
const [quickOpts, setOptions] = useState<GeneratorOptions>(generatorOptions);
|
||||
const checkedOptions = useMemo(
|
||||
() => Object.keys(quickOpts).filter(k => quickOpts[k as keyof GeneratorOptions] as boolean),
|
||||
[quickOpts]
|
||||
);
|
||||
|
||||
const onCheckedValueChange = useCallback((_: unknown, e: fui.MenuCheckedValueChangeData): void =>
|
||||
{
|
||||
const opts: Partial<Omit<GeneratorOptions, "Length" | "IncludeCustomSet" | "ExcludeCustomSet">> = {};
|
||||
|
||||
const keys = Object.keys(quickOpts)
|
||||
.filter(i =>
|
||||
i !== "Length" && i !== "IncludeCustomSet" && i !== "ExcludeCustomSet" &&
|
||||
i.startsWith("Exclude") === (e.name === "exclude")
|
||||
) as (keyof Omit<GeneratorOptions, "Length" | "IncludeCustomSet" | "ExcludeCustomSet">)[];
|
||||
|
||||
for (const key of keys)
|
||||
opts[key] = e.checkedItems.includes(key);
|
||||
|
||||
setOptions({ ...generatorOptions, ...quickOpts, ...opts });
|
||||
}, [quickOpts]);
|
||||
|
||||
useEffect(() => onChange(quickOpts), [onChange, quickOpts]);
|
||||
|
||||
const IncludeIcon: ic.FluentIcon = ic.bundleIcon(ic.AddCircleFilled, ic.AddCircleRegular);
|
||||
const ExcludeIcon: ic.FluentIcon = ic.bundleIcon(ic.SubtractCircleFilled, ic.SubtractCircleRegular);
|
||||
const cls = useStyles();
|
||||
|
||||
return (
|
||||
<div className={ cls.options }>
|
||||
|
||||
<div className={ cls.lengthContainer }>
|
||||
<fui.Slider
|
||||
min={ extOptions.MinLength } max={ Math.max(extOptions.MaxLength, generatorOptions.Length) }
|
||||
value={ quickOpts.Length } onChange={ (_, e) => setOptions({ ...quickOpts, Length: e.value }) } />
|
||||
<fui.Text>{ quickOpts.Length }</fui.Text>
|
||||
</div>
|
||||
|
||||
<div className={ cls.characterOptionsContainer }>
|
||||
<fui.Menu
|
||||
positioning={ { position: "after", align: "center", offset: -48 } }
|
||||
checkedValues={ { include: checkedOptions } }
|
||||
onCheckedValueChange={ onCheckedValueChange }>
|
||||
|
||||
<fui.MenuTrigger disableButtonEnhancement>
|
||||
<fui.MenuButton appearance="subtle" icon={ <IncludeIcon /> }>
|
||||
{ i18n.t("popup.include") }
|
||||
</fui.MenuButton>
|
||||
</fui.MenuTrigger>
|
||||
|
||||
<fui.MenuPopover>
|
||||
<fui.MenuList>
|
||||
<fui.MenuItemCheckbox name="include" value="Uppercase">
|
||||
{ i18n.t("common.characters.uppercase") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="include" value="Lowercase">
|
||||
{ i18n.t("common.characters.lowercase") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="include" value="Numeric">
|
||||
{ i18n.t("common.characters.numeric") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="include" value="Special">
|
||||
{ i18n.t("common.characters.special") }
|
||||
</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.MenuPopover>
|
||||
</fui.Menu>
|
||||
|
||||
<fui.Menu
|
||||
positioning={ { position: "after", align: "center", offset: -64 } }
|
||||
checkedValues={ { exclude: checkedOptions } }
|
||||
onCheckedValueChange={ onCheckedValueChange }>
|
||||
|
||||
<fui.MenuTrigger disableButtonEnhancement>
|
||||
<fui.MenuButton appearance="subtle" icon={ <ExcludeIcon /> }>
|
||||
{ i18n.t("popup.exclude") }
|
||||
</fui.MenuButton>
|
||||
</fui.MenuTrigger>
|
||||
|
||||
<fui.MenuPopover>
|
||||
<fui.MenuList>
|
||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeSimilar">
|
||||
{ i18n.t("common.characters.similar") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeAmbiguous" disabled={ !quickOpts.Special }>
|
||||
{ i18n.t("common.characters.ambiguous") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeRepeating">
|
||||
{ i18n.t("common.characters.repeating.label") }
|
||||
</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.MenuPopover>
|
||||
</fui.Menu>
|
||||
|
||||
<fui.Tooltip content={ i18n.t("common.actions.reset") } relationship="label">
|
||||
<fui.Button appearance="subtle" icon={ <ic.ArrowUndoRegular /> } onClick={ () => setOptions(generatorOptions) } />
|
||||
</fui.Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type QuickOptionsProps =
|
||||
{
|
||||
onChange: (value: GeneratorOptions) => void;
|
||||
};
|
||||
@@ -1,17 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Password generator</title>
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
|
||||
<script type="module" src="./popup.tsx"></script>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
html,
|
||||
body
|
||||
{
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
max-height: 600px;
|
||||
overflow-y: hidden;
|
||||
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@keyframes scaleUpIn
|
||||
{
|
||||
from
|
||||
{
|
||||
transform: scale(0.5);
|
||||
filter: opacity(0);
|
||||
}
|
||||
|
||||
to
|
||||
{
|
||||
transform: scale(1);
|
||||
filter: opacity(1)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin
|
||||
{
|
||||
from
|
||||
{
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to
|
||||
{
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn
|
||||
{
|
||||
from
|
||||
{
|
||||
filter: opacity(0);
|
||||
}
|
||||
|
||||
to
|
||||
{
|
||||
filter: opacity(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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 ReactDOM from "react-dom/client";
|
||||
import App from "../../shared/App";
|
||||
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(
|
||||
<React.StrictMode>
|
||||
<App>
|
||||
<Popup />
|
||||
</App>
|
||||
</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,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import jsConfigs from "@eslint/js";
|
||||
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import globals from "globals";
|
||||
import autoImports from "./.wxt/eslint-auto-imports.mjs";
|
||||
|
||||
export default [
|
||||
autoImports,
|
||||
jsConfigs.configs.recommended,
|
||||
reactPlugin.configs.flat.recommended,
|
||||
{
|
||||
ignores: [".wxt/", ".output/"],
|
||||
},
|
||||
{
|
||||
files: ["**/*.{ts,tsx,js,mjs,jsx}"],
|
||||
languageOptions:
|
||||
{
|
||||
parser: tsParser,
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
...globals.browser
|
||||
},
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect"
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tsPlugin
|
||||
}
|
||||
},
|
||||
{
|
||||
rules:
|
||||
{
|
||||
"no-unused-vars": "off",
|
||||
"no-undef": "error",
|
||||
"semi": ["error", "always"],
|
||||
"quotes": ["error", "double"],
|
||||
"indent": ["warn", "tab", { "SwitchCase": 1 }],
|
||||
"no-empty": "off",
|
||||
"react/prop-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||
"argsIgnorePattern": "^_",
|
||||
"args": "none"
|
||||
}],
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,82 @@
|
||||
manifest:
|
||||
name: "Password generator"
|
||||
description: "Extension which helps you to easily generate strong and customizable passwords in a few clicks"
|
||||
author: "Eugene Fox"
|
||||
|
||||
common:
|
||||
actions:
|
||||
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:
|
||||
too_short: "Password length must be at least 4 characters"
|
||||
no_characters: "At least one set of characters must be selected"
|
||||
too_long: "Length is too long to generate password with unique characters"
|
||||
|
||||
about:
|
||||
title: "About"
|
||||
developed_by: "Developed by Eugene Fox"
|
||||
licensed_under: "Licensed under"
|
||||
mit_license: "MIT License"
|
||||
translation_cta:
|
||||
text: "Found a typo or want a translation for your language?"
|
||||
button: "Get started here"
|
||||
links:
|
||||
website: "My website"
|
||||
source: "Source code"
|
||||
changelog: "Changelog"
|
||||
cta:
|
||||
feedback: "Leave feedback"
|
||||
sponsor: "Buy me a coffee"
|
||||
|
||||
popup:
|
||||
refresh: "Generate new"
|
||||
include: "Include"
|
||||
exclude: "Exclude"
|
||||
advanced: "Advanced"
|
||||
|
||||
settings:
|
||||
title: "Settings"
|
||||
length:
|
||||
title: "Default password length"
|
||||
hint: "Recommended length: 8–16"
|
||||
quick_range: "Quick adjustment length range"
|
||||
exclude_custom: "Do not use these characters:"
|
||||
|
||||
advanced:
|
||||
title: "Advanced password generator"
|
||||
saved_msg: "Configuration saved"
|
||||
copied_msg: "Copied to clipboard"
|
||||
actions:
|
||||
generate: "Generate"
|
||||
save_config: "Save configuration"
|
||||
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"
|
||||
@@ -0,0 +1,82 @@
|
||||
manifest:
|
||||
name: "Generator haseł"
|
||||
description: "Rozszerzenie, które pomaga w łatwy sposób generować silne i dostosowane hasła w kilku kliknięciach"
|
||||
author: "Jewgienij Lis"
|
||||
|
||||
common:
|
||||
actions:
|
||||
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:
|
||||
too_short: "Minimalna długość hasła to 4 znaki"
|
||||
no_characters: "Musisz wybrać przynajmniej jeden rodzaj znaków"
|
||||
too_long: "Wybrana długość jest zbyt duża, aby wygenerować unikalne znaki"
|
||||
|
||||
about:
|
||||
title: "O rozszerzeniu"
|
||||
developed_by: "Autor: Jewgienij Lis"
|
||||
licensed_under: ""
|
||||
mit_license: "Licencja MIT"
|
||||
translation_cta:
|
||||
text: "Znalazłeś błąd lub potrzebujesz tłumaczenia na swój język?"
|
||||
button: "Zacznij tutaj"
|
||||
links:
|
||||
website: "Moja strona internetowa"
|
||||
source: "Kod źródłowy"
|
||||
changelog: "Lista zmian"
|
||||
cta:
|
||||
feedback: "Zostaw opinię"
|
||||
sponsor: "Wesprzyj"
|
||||
|
||||
popup:
|
||||
refresh: "Wygeneruj nowe"
|
||||
include: "Dodaj"
|
||||
exclude: "Wyklucz"
|
||||
advanced: "Zaawansowane"
|
||||
|
||||
settings:
|
||||
title: "Ustawienia"
|
||||
length:
|
||||
title: "Domyślna długość hasła"
|
||||
hint: "Zalecana długość: 8–16"
|
||||
quick_range: "Zakres długości dla szybkich ustawień"
|
||||
exclude_custom: "Nie używaj tych znaków:"
|
||||
|
||||
advanced:
|
||||
title: "Zaawansowany generator haseł"
|
||||
saved_msg: "Konfiguracja zapisana"
|
||||
copied_msg: "Skopiowano do schowka"
|
||||
actions:
|
||||
generate: "Wygeneruj"
|
||||
save_config: "Zapisz konfigurację"
|
||||
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"
|
||||
@@ -0,0 +1,82 @@
|
||||
manifest:
|
||||
name: "Gerador de Senhas"
|
||||
description: "Extensão que ajuda você a gerar senhas fortes e personalizadas facilmente em poucos cliques"
|
||||
author: "Eugênio Fox"
|
||||
|
||||
common:
|
||||
actions:
|
||||
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:
|
||||
too_short: "O comprimento da senha deve ter pelo menos 4 caracteres"
|
||||
no_characters: "É necessário selecionar pelo menos um conjunto de caracteres"
|
||||
too_long: "O comprimento é muito longo para gerar uma senha com caracteres únicos"
|
||||
|
||||
about:
|
||||
title: "Sobre"
|
||||
developed_by: "Desenvolvido por Eugênio Fox"
|
||||
licensed_under: "Licenciado sob"
|
||||
mit_license: "Licença MIT"
|
||||
translation_cta:
|
||||
text: "Encontrou um erro ou quer uma tradução no seu idioma?"
|
||||
button: "Comece aqui"
|
||||
links:
|
||||
website: "Meu site"
|
||||
source: "Código fonte"
|
||||
changelog: "Registro de alterações"
|
||||
cta:
|
||||
feedback: "Enviar comentários"
|
||||
sponsor: "Pague um café"
|
||||
|
||||
popup:
|
||||
refresh: "Gerar nova"
|
||||
include: "Incluir"
|
||||
exclude: "Excluir"
|
||||
advanced: "Avançado"
|
||||
|
||||
settings:
|
||||
title: "Configurações"
|
||||
length:
|
||||
title: "Comprimento padrão da senha"
|
||||
hint: "Comprimento recomendado: 8–16"
|
||||
quick_range: "Intervalo do comprimento de ajuste rápido"
|
||||
exclude_custom: "Não use esses caracteres:"
|
||||
|
||||
advanced:
|
||||
title: "Gerador de senha avançado"
|
||||
saved_msg: "Configuração salva"
|
||||
copied_msg: "Copiado para a área de transferência"
|
||||
actions:
|
||||
generate: "Gerar"
|
||||
save_config: "Salvar configuração"
|
||||
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"
|
||||
@@ -0,0 +1,82 @@
|
||||
manifest:
|
||||
name: "Генератор паролей"
|
||||
description: "Расширение, которое поможет вам легко создавать надежные и настраиваемые пароли всего в несколько кликов"
|
||||
author: "Евгений Лис"
|
||||
|
||||
common:
|
||||
actions:
|
||||
reset: "Сбросить"
|
||||
copy: "Копировать"
|
||||
characters:
|
||||
uppercase: "Прописные"
|
||||
lowercase: "Строчные"
|
||||
numeric: "Числовые"
|
||||
special: "Специальные"
|
||||
similar: "Похожие"
|
||||
ambiguous: "Особые"
|
||||
repeating:
|
||||
label: "Повторяющиеся"
|
||||
hint: "Каждый символ пароля будет уникальным"
|
||||
custom: "Пользовательские"
|
||||
sections:
|
||||
include: "Добавить символы"
|
||||
exclude: "Исключить символы"
|
||||
|
||||
errors:
|
||||
too_short: "Минимальная длина пароля — 4 символа"
|
||||
no_characters: "Необходимо выбрать хотя бы один вид символов"
|
||||
too_long: "Выбранная длина слишком большая, для генерации уникальных символов"
|
||||
|
||||
about:
|
||||
title: "О расширении"
|
||||
developed_by: "Разработчик: Евгений Лис"
|
||||
licensed_under: ""
|
||||
mit_license: "Лицензия MIT"
|
||||
translation_cta:
|
||||
text: "Нашли опечатку или хотите перевод на ваш язык?"
|
||||
button: "Начните здесь"
|
||||
links:
|
||||
website: "Мой веб-сайт"
|
||||
source: "Исходный код"
|
||||
changelog: "Список изменений"
|
||||
cta:
|
||||
feedback: "Оставить отзыв"
|
||||
sponsor: "Поддержать"
|
||||
|
||||
popup:
|
||||
refresh: "Сгенерировать новый"
|
||||
include: "Добавить"
|
||||
exclude: "Исключить"
|
||||
advanced: "Расширенный"
|
||||
|
||||
settings:
|
||||
title: "Настройки"
|
||||
length:
|
||||
title: "Длина пароля по умолчанию"
|
||||
hint: "Рекомендованная длина: 8–16"
|
||||
quick_range: "Диапазон длины для быстрых настроек"
|
||||
exclude_custom: "Не использовать эти символы:"
|
||||
|
||||
advanced:
|
||||
title: "Расширенный генератор паролей"
|
||||
saved_msg: "Конфигурация сохранена"
|
||||
copied_msg: "Скопировано в буфер обмена"
|
||||
actions:
|
||||
generate: "Сгенерировать"
|
||||
save_config: "Сохранить конфигурацию"
|
||||
copy_all: "Копировать все"
|
||||
password:
|
||||
title: "Генератор паролей"
|
||||
length: "Длина пароля"
|
||||
min_of_type: "Не менее"
|
||||
passphrase:
|
||||
title: "Парольных фраз"
|
||||
length: "Количество слов"
|
||||
replace: "Заменить случайные символы"
|
||||
random_case: "Случайный регистр символов"
|
||||
allow_repat:
|
||||
label: "Разрешить повторение"
|
||||
hint: "Если отключено, каждое слово в фразе будет уникальным"
|
||||
separate_words:
|
||||
label: "Разделить слова"
|
||||
hint: "Если включено, указанная последовательность будет вставлена между каждым словом. Если поле пустое - будет использован пробел"
|
||||
@@ -0,0 +1,82 @@
|
||||
manifest:
|
||||
name: "Генератор паролів"
|
||||
description: "Розширення, яке допоможе вам легко створювати надійні та налаштовані паролі всього в декілька клацань"
|
||||
author: "Євген Лис"
|
||||
|
||||
common:
|
||||
actions:
|
||||
reset: "Скинути"
|
||||
copy: "Копіювати"
|
||||
characters:
|
||||
uppercase: "Великі"
|
||||
lowercase: "Малі"
|
||||
numeric: "Числові"
|
||||
special: "Спеціальні"
|
||||
similar: "Схожі"
|
||||
ambiguous: "Особливі"
|
||||
repeating:
|
||||
label: "Повторювані"
|
||||
hint: "Кожен символ пароля буде унікальним"
|
||||
custom: "Користувацькі"
|
||||
sections:
|
||||
include: "Додати символи"
|
||||
exclude: "Виключити символи"
|
||||
|
||||
errors:
|
||||
too_short: "Мінімальна довжина пароля — 4 символи"
|
||||
no_characters: "Необхідно вибрати хоча б один вид символів"
|
||||
too_long: "Вибрана довжина занадто велика, для генерації унікальних символів"
|
||||
|
||||
about:
|
||||
title: "Про розширення"
|
||||
developed_by: "Розробник: Євген Лис"
|
||||
licensed_under: ""
|
||||
mit_license: "Ліцензія MIT"
|
||||
translation_cta:
|
||||
text: "Знайшли помилку або хочете переклад на вашу мову?"
|
||||
button: "Почніть тут"
|
||||
links:
|
||||
website: "Мій веб-сайт"
|
||||
source: "Вихідний код"
|
||||
changelog: "Список змін"
|
||||
cta:
|
||||
feedback: "Залишити відгук"
|
||||
sponsor: "Підтримати"
|
||||
|
||||
popup:
|
||||
refresh: "Згенерувати новий"
|
||||
include: "Додати"
|
||||
exclude: "Виключити"
|
||||
advanced: "Розширений"
|
||||
|
||||
settings:
|
||||
title: "Налаштування"
|
||||
length:
|
||||
title: "Довжина пароля за замовчуванням"
|
||||
hint: "Рекомендована довжина: 8–16"
|
||||
quick_range: "Діапазон довжини для швидких налаштувань"
|
||||
exclude_custom: "Не використовувати ці символи:"
|
||||
|
||||
advanced:
|
||||
title: "Розширений генератор паролів"
|
||||
saved_msg: "Конфігурація збережена"
|
||||
copied_msg: "Скопійовано у буфер обміну"
|
||||
actions:
|
||||
generate: "Згенерувати"
|
||||
save_config: "Зберегти конфігурацію"
|
||||
copy_all: "Копіювати все"
|
||||
password:
|
||||
title: "Генератор паролів"
|
||||
length: "Довжина пароля"
|
||||
min_of_type: "Не менше"
|
||||
passphrase:
|
||||
title: "Парольних фраз"
|
||||
length: "Кількість слів"
|
||||
replace: "Замінити випадкові символи"
|
||||
random_case: "Випадковий регістр символів"
|
||||
allow_repat:
|
||||
label: "Дозволити повторення"
|
||||
hint: "Якщо вимкнено, кожне слово у фразі буде унікальним"
|
||||
separate_words:
|
||||
label: "Розділити слова"
|
||||
hint: "Якщо увімкнено, вказана послідовність буде вставлена між кожним словом. Якщо поле порожнє - буде використано пробіл"
|
||||
@@ -0,0 +1,82 @@
|
||||
manifest:
|
||||
name: "密码生成器"
|
||||
description: "这个扩展可以帮助您轻松生成强大且可定制的密码"
|
||||
author: "Eugene Fox"
|
||||
|
||||
common:
|
||||
actions:
|
||||
reset: "重置"
|
||||
copy: "复制"
|
||||
characters:
|
||||
uppercase: "大写字母"
|
||||
lowercase: "小写字母"
|
||||
numeric: "数字"
|
||||
special: "特殊"
|
||||
similar: "相似"
|
||||
ambiguous: "易混淆"
|
||||
repeating:
|
||||
label: "重复"
|
||||
hint: "密码中的每个字符都将是独一无二的"
|
||||
custom: "自定义"
|
||||
sections:
|
||||
include: "包括字符"
|
||||
exclude: "排除字符"
|
||||
|
||||
errors:
|
||||
too_short: "密码的长度必须至少为4个字符"
|
||||
no_characters: "必须至少选择一组字符"
|
||||
too_long: "长度太长,无法生成具有唯一字符的密码"
|
||||
|
||||
about:
|
||||
title: "关于"
|
||||
developed_by: "由 Eugene Fox 开发"
|
||||
licensed_under: "协议:"
|
||||
mit_license: "MIT License"
|
||||
translation_cta:
|
||||
text: "发现错别字或需要翻译成您的语言?"
|
||||
button: "从这里开始"
|
||||
links:
|
||||
website: "我的网站"
|
||||
source: "源代码"
|
||||
changelog: "更新日志"
|
||||
cta:
|
||||
feedback: "提供反馈"
|
||||
sponsor: "赞助开发者"
|
||||
|
||||
popup:
|
||||
refresh: "刷新"
|
||||
include: "包括"
|
||||
exclude: "排除"
|
||||
advanced: "高级"
|
||||
|
||||
settings:
|
||||
title: "设置"
|
||||
length:
|
||||
title: "默认密码长度"
|
||||
hint: "建议长度:8-16"
|
||||
quick_range: "快速调节长度范围"
|
||||
exclude_custom: "不要使用这些字符:"
|
||||
|
||||
advanced:
|
||||
title: "高级密码生成器"
|
||||
saved_msg: "配置已保存"
|
||||
copied_msg: "已复制到剪贴板"
|
||||
actions:
|
||||
generate: "生成"
|
||||
save_config: "保存配置"
|
||||
copy_all: "复制全部"
|
||||
password:
|
||||
title: "密码生成器"
|
||||
length: "密码长度"
|
||||
min_of_type: "最少字符数"
|
||||
passphrase:
|
||||
title: "密码短语生成器"
|
||||
length: "单词数量"
|
||||
replace: "替换随机字符"
|
||||
random_case: "随机大小写"
|
||||
allow_repat:
|
||||
label: "允许重复"
|
||||
hint: "如果禁用,密码短语中的每个单词都是唯一的"
|
||||
separate_words:
|
||||
label: "用以下字符分隔单词"
|
||||
hint: "如果启用,提供的序列将放置在每个单词之间。如果字段为空,将使用空格"
|
||||
@@ -1,39 +1,41 @@
|
||||
{
|
||||
"name": "password-generator",
|
||||
"version": "3.1.4",
|
||||
"version": "5.0.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||
"dev": "wxt",
|
||||
"build": "wxt build --mv3",
|
||||
"zip": "wxt zip --mv3",
|
||||
"lint": "eslint . -c eslint.config.js",
|
||||
"prebuild": "npm run lint",
|
||||
"prezip": "npm run lint",
|
||||
"prepare": "wxt prepare",
|
||||
"compile": "tsc --noEmit",
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.53.0",
|
||||
"@fluentui/react-icons": "^2.0.242",
|
||||
"@fluentui/react-components": "^9.63.0",
|
||||
"@fluentui/react-icons": "^2.0.298",
|
||||
"@wxt-dev/i18n": "^0.2.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react-dom": "^18.3.1",
|
||||
"react-responsive": "^10.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/webextension-polyfill": "^0.10.7",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.12",
|
||||
"vite-plugin-static-copy": "^1.0.5",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-plugin-web-extension": "^4.1.4",
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"@wxt-dev/module-react": "^1.1.3",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
"wxt": "^0.20.6"
|
||||
},
|
||||
"resolutions": {
|
||||
"scheduler": "^0.20.0",
|
||||
"@floating-ui/dom": "^1.5.4"
|
||||
"overrides": {
|
||||
"@babel/runtime": "^7.26.10"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,28 @@
|
||||
import { StorageProvider } from "@/utils/storage";
|
||||
import { useTheme } from "@/utils/useTheme";
|
||||
import { FluentProvider, makeStyles, Spinner, Theme } from "@fluentui/react-components";
|
||||
import Snow from "./specials/Snow";
|
||||
|
||||
const App: React.FC<React.PropsWithChildren> = props =>
|
||||
{
|
||||
const theme: Theme = useTheme();
|
||||
const cls = useStyles();
|
||||
|
||||
return (
|
||||
<FluentProvider theme={ theme }>
|
||||
<StorageProvider loader={ <Spinner size="large" className={ cls.spinner } /> }>
|
||||
{ props.children }
|
||||
<Snow />
|
||||
</StorageProvider>
|
||||
</FluentProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
spinner:
|
||||
{
|
||||
height: "120px",
|
||||
},
|
||||
});
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto 1fr",
|
||||
alignItems: "center",
|
||||
},
|
||||
label:
|
||||
{
|
||||
color: tokens.colorNeutralForeground2,
|
||||
cursor: "pointer",
|
||||
justifySelf: "start",
|
||||
},
|
||||
labelUnchecked:
|
||||
{
|
||||
"&:hover":
|
||||
{
|
||||
color: tokens.colorNeutralForeground2Hover,
|
||||
|
||||
"&:active":
|
||||
{
|
||||
color: tokens.colorNeutralForeground2Pressed,
|
||||
},
|
||||
},
|
||||
},
|
||||
labelLeft:
|
||||
{
|
||||
justifySelf: "end",
|
||||
textAlign: "right",
|
||||
},
|
||||
labelChecked:
|
||||
{
|
||||
fontWeight: tokens.fontWeightSemibold,
|
||||
color: tokens.colorNeutralForeground1,
|
||||
|
||||
"&:hover":
|
||||
{
|
||||
color: tokens.colorNeutralForeground1Hover,
|
||||
|
||||
"&:active":
|
||||
{
|
||||
color: tokens.colorNeutralForeground1Pressed,
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Label, LabelProps, mergeClasses, Switch, SwitchOnChangeData, SwitchProps } from "@fluentui/react-components";
|
||||
import { ReactElement } from "react";
|
||||
import { useStyles } from "./DoubleLabeledSwitch.styles";
|
||||
|
||||
export default function DoubleLabledSwitch(props: DoubleLabledSwitchProps): ReactElement
|
||||
{
|
||||
const [isOn, setOn] = useState<boolean>(props.checked ?? props.defaultChecked ?? false);
|
||||
const cls = useStyles();
|
||||
const switchRef = useRef<HTMLInputElement | null>();
|
||||
|
||||
const setChecked = useCallback((checked: boolean) =>
|
||||
{
|
||||
if (!switchRef.current || isOn === checked)
|
||||
return;
|
||||
|
||||
switchRef.current.click();
|
||||
}, [switchRef.current, isOn]);
|
||||
|
||||
const onChange = useCallback((ev: React.ChangeEvent<HTMLInputElement>, data: SwitchOnChangeData) =>
|
||||
{
|
||||
setOn(data.checked);
|
||||
props.onChange?.(ev, data);
|
||||
}, [props.onChange]);
|
||||
|
||||
return (
|
||||
<div { ...props.outerRoot } className={ mergeClasses(cls.root, props.outerRoot?.className) }>
|
||||
|
||||
<Label
|
||||
onClick={ () => setChecked(false) }
|
||||
{ ...props.offLabelProps }
|
||||
className={ mergeClasses(
|
||||
cls.label,
|
||||
cls.labelLeft,
|
||||
!isOn ? cls.labelChecked : cls.labelUnchecked,
|
||||
props.offLabelProps?.className
|
||||
) }>
|
||||
|
||||
{ props.offLabel }
|
||||
</Label>
|
||||
|
||||
<Switch { ...props } ref={ (input) => switchRef.current = input } onChange={ onChange } />
|
||||
|
||||
<Label
|
||||
onClick={ () => setChecked(true) }
|
||||
{ ...props.onLabelProps }
|
||||
className={ mergeClasses(
|
||||
cls.label,
|
||||
isOn ? cls.labelChecked : cls.labelUnchecked,
|
||||
props.onLabelProps?.className
|
||||
) }>
|
||||
|
||||
{ props.onLabel }
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type DoubleLabledSwitchProps = Omit<SwitchProps, "label"> &
|
||||
{
|
||||
offLabel?: string;
|
||||
onLabel?: string;
|
||||
outerRoot?: React.HTMLAttributes<HTMLDivElement>;
|
||||
offLabelProps?: LabelProps;
|
||||
onLabelProps?: LabelProps;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
@keyframes snowfall
|
||||
{
|
||||
0%
|
||||
{
|
||||
transform: translate3d(var(--left-start), 0, 0);
|
||||
filter: opacity(.6);
|
||||
}
|
||||
|
||||
100%
|
||||
{
|
||||
transform: translate3d(var(--left-end), 610px, 0);
|
||||
filter: opacity(0);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { GriffelStyle, makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
||||
import { GriffelStyle, makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
const random = (max: number) => Math.floor(Math.random() * max);
|
||||
const random = (max: number): number => Math.floor(Math.random() * max);
|
||||
|
||||
export const useStyles = (count: number) => makeStyles({
|
||||
export const SNOWFLAKES_NUM: number = 100;
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
snow:
|
||||
{
|
||||
position: "absolute",
|
||||
...shorthands.overflow("hidden"),
|
||||
overflow: "hidden",
|
||||
pointerEvents: "none",
|
||||
|
||||
top: 0,
|
||||
@@ -20,16 +22,16 @@ export const useStyles = (count: number) => makeStyles({
|
||||
width: "var(--size)",
|
||||
height: "var(--size)",
|
||||
backgroundColor: tokens.colorScrollbarOverlay,
|
||||
...shorthands.borderRadius(tokens.borderRadiusCircular),
|
||||
borderRadius: tokens.borderRadiusCircular,
|
||||
position: "absolute",
|
||||
top: "-5px",
|
||||
},
|
||||
...[...Array(count)].reduce(
|
||||
...[...Array(SNOWFLAKES_NUM)].reduce(
|
||||
(acc, _, i): Record<string, GriffelStyle> => ({
|
||||
...acc,
|
||||
[`snowflake-${i}`]: {
|
||||
"--size": `${random(5)}px`,
|
||||
"--left-ini": `${random(20) - 10}vw`,
|
||||
"--left-start": `${random(20) - 10}vw`,
|
||||
"--left-end": `${random(20) - 10}vw`,
|
||||
left: `${random(100)}vw`,
|
||||
animationName: "snowfall",
|
||||
@@ -0,0 +1,21 @@
|
||||
import { mergeClasses } from "@fluentui/react-components";
|
||||
import { SNOWFLAKES_NUM, useStyles } from "./Snow.styles";
|
||||
import "./Snow.css";
|
||||
|
||||
const Snow: React.FC = () =>
|
||||
{
|
||||
const cls = useStyles();
|
||||
|
||||
if (![0, 11].includes(new Date().getMonth()))
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className={ cls.snow }>
|
||||
{ [...Array(SNOWFLAKES_NUM)].map((_, i) =>
|
||||
<div key={ i } className={ mergeClasses(cls.snowflake, cls[`snowflake-${i}`]) } />
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Snow;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
...shorthands.padding(tokens.spacingVerticalM, tokens.spacingHorizontalS),
|
||||
},
|
||||
spinner:
|
||||
{
|
||||
alignSelf: "center",
|
||||
...shorthands.margin(tokens.spacingVerticalXXXL, 0),
|
||||
},
|
||||
accordionAnimation:
|
||||
{
|
||||
"> .fui-AccordionItem .fui-AccordionHeader__expandIcon > svg":
|
||||
{
|
||||
transitionProperty: "transform",
|
||||
transitionDuration: tokens.durationNormal,
|
||||
transitionTimingFunction: tokens.curveDecelerateMax,
|
||||
},
|
||||
"> .fui-AccordionItem > .fui-AccordionPanel":
|
||||
{
|
||||
animationName: "fadeIn",
|
||||
animationDuration: tokens.durationSlow,
|
||||
animationTimingFunction: tokens.curveDecelerateMin,
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Accordion, FluentProvider, Spinner } from "@fluentui/react-components";
|
||||
import { useStyles } from "./App.styles";
|
||||
import AboutSection from "./Components/AboutSection";
|
||||
import GeneratorView from "./Components/GeneratorView";
|
||||
import SettingsSection from "./Components/SettingsSection";
|
||||
import Specials from "./Specials/Specials";
|
||||
import { StorageProvider } from "./Utils/Storage";
|
||||
import { useTheme } from "./Utils/Theme";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function App(): JSX.Element
|
||||
{
|
||||
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
|
||||
className={ cls.accordionAnimation }>
|
||||
<SettingsSection />
|
||||
<AboutSection />
|
||||
</Accordion>
|
||||
</StorageProvider>
|
||||
<Specials />
|
||||
</main>
|
||||
</FluentProvider>
|
||||
);
|
||||
}
|
||||
@@ -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 * as fui from "@fluentui/react-components";
|
||||
import { InfoRegular, PersonFeedbackRegular } from "@fluentui/react-icons";
|
||||
import Package from "../../package.json";
|
||||
import BuyMeACoffee from "../Assets/BuyMeACoffee.svg?react";
|
||||
import { bmcDarkTheme, bmcLightTheme } from "../Data/BmcTheme";
|
||||
import { GetFeedbackLink, GithubLink, PersonalLink } from "../Data/Links";
|
||||
import { GetLocaleString as loc } from "../Utils/Localization";
|
||||
import { useTheme } from "../Utils/Theme";
|
||||
import { useStyles } from "./AboutSection.styles";
|
||||
|
||||
export default function AboutSection(): JSX.Element
|
||||
{
|
||||
const bmcTheme = useTheme(bmcLightTheme, bmcDarkTheme);
|
||||
const cls = useStyles();
|
||||
|
||||
const link = (text: string, href: string): JSX.Element => (
|
||||
<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
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<fui.AccordionItem value="about">
|
||||
<fui.AccordionHeader as="h2" icon={ <InfoRegular /> }>{ loc("about@title") }</fui.AccordionHeader>
|
||||
<fui.AccordionPanel className={ cls.root }>
|
||||
<header className={ cls.horizontalContainer }>
|
||||
<fui.Subtitle1 as="h1">{ loc("name") }</fui.Subtitle1>
|
||||
<fui.Caption1 as="span">v{ Package.version }</fui.Caption1>
|
||||
</header>
|
||||
|
||||
<fui.Text as="p">
|
||||
{ loc("about@developedBy") } ({ link("@xfox111", PersonalLink.Twitter) })
|
||||
<br />
|
||||
{ loc("about@licensedUnder") } { link(loc("about@mitLicense"), GithubLink.License) }
|
||||
</fui.Text>
|
||||
|
||||
<fui.Text as="p">
|
||||
{ loc("about@translationCta") }<br />
|
||||
{ link(loc("about@translationCtaButton"), GithubLink.TranslationGuide) }
|
||||
</fui.Text>
|
||||
|
||||
<fui.Text as="p">
|
||||
{ link(loc("about@website"), PersonalLink.Website) } <br />
|
||||
{ link(loc("about@sourceCode"), GithubLink.Repository) } <br />
|
||||
{ link(loc("about@changelog"), GithubLink.Changelog) }
|
||||
</fui.Text>
|
||||
|
||||
<div className={ cls.horizontalContainer }>
|
||||
<fui.Button { ...buttonProps(GetFeedbackLink(), <PersonFeedbackRegular />) }>
|
||||
{ loc("about@feedback") }
|
||||
</fui.Button>
|
||||
<fui.FluentProvider theme={ bmcTheme }>
|
||||
<fui.Button { ...buttonProps(PersonalLink.BuyMeACoffee, <BuyMeACoffee />) }>
|
||||
{ loc("about@sponsor") }
|
||||
</fui.Button>
|
||||
</fui.FluentProvider>
|
||||
</div>
|
||||
</fui.AccordionPanel>
|
||||
</fui.AccordionItem>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
input:
|
||||
{
|
||||
fontFamily: tokens.fontFamilyMonospace,
|
||||
},
|
||||
lengthContainer:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
paddingRight: tokens.spacingHorizontalM,
|
||||
},
|
||||
copyIcon:
|
||||
{
|
||||
animationName: "scaleUpIn",
|
||||
animationDuration: tokens.durationSlow,
|
||||
animationTimingFunction: tokens.curveEasyEaseMax,
|
||||
},
|
||||
refreshIcon:
|
||||
{
|
||||
animationName: "spin",
|
||||
animationDuration: tokens.durationSlow,
|
||||
animationTimingFunction: tokens.curveEasyEaseMax,
|
||||
},
|
||||
msgBar:
|
||||
{
|
||||
...shorthands.padding(tokens.spacingVerticalMNudge, tokens.spacingHorizontalM),
|
||||
},
|
||||
options:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
...shorthands.padding(tokens.spacingVerticalS, tokens.spacingHorizontalS),
|
||||
},
|
||||
characterOptionsContainer:
|
||||
{
|
||||
display: "flex",
|
||||
...shorthands.gap(tokens.spacingHorizontalXS),
|
||||
},
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import * as Icons from "@fluentui/react-icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import GeneratorOptions from "../Models/GeneratorOptions";
|
||||
import { GetLocaleString as loc } from "../Utils/Localization";
|
||||
import { GeneratePassword } from "../Utils/PasswordGenerator";
|
||||
import { useStorage } from "../Utils/Storage";
|
||||
import { useTimeout } from "../Utils/Timeout";
|
||||
import { useStyles } from "./GeneratorView.styles";
|
||||
|
||||
export default function GeneratorView(props: { collapse: boolean; }): JSX.Element
|
||||
{
|
||||
const { generatorOptions, extOptions } = useStorage();
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [quickOpts, _setOpts] = useState<GeneratorOptions>(generatorOptions);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [refreshTimer, copyTimer] = [useTimeout(310), useTimeout(1000)];
|
||||
const checkedOptions = Object.keys(quickOpts).filter(k => quickOpts[k as keyof GeneratorOptions] as boolean);
|
||||
const cls = useStyles();
|
||||
|
||||
const IncludeIcon: Icons.FluentIcon = Icons.bundleIcon(Icons.AddCircleFilled, Icons.AddCircleRegular);
|
||||
const ExcludeIcon: Icons.FluentIcon = Icons.bundleIcon(Icons.SubtractCircleFilled, Icons.SubtractCircleRegular);
|
||||
|
||||
const resetOptions = (): void =>
|
||||
_setOpts(generatorOptions);
|
||||
|
||||
const setOptions = (opts: Partial<GeneratorOptions>) =>
|
||||
_setOpts({ ...quickOpts, ...opts });
|
||||
|
||||
function RefreshPassword(): void
|
||||
{
|
||||
setError(null);
|
||||
const options: GeneratorOptions = { ...generatorOptions, ...quickOpts };
|
||||
|
||||
try { setPassword(GeneratePassword(options)); }
|
||||
catch (e) { setError((e as Error).message); }
|
||||
}
|
||||
|
||||
async function CopyPassword(): Promise<void>
|
||||
{
|
||||
await window.navigator.clipboard.writeText(password);
|
||||
copyTimer.trigger();
|
||||
}
|
||||
|
||||
function OnCheckedValueChange(_: unknown, e: fui.MenuCheckedValueChangeData): void
|
||||
{
|
||||
const opts: Partial<Omit<GeneratorOptions, "Length">> = {};
|
||||
|
||||
let keys = Object.keys(quickOpts).filter(i => i !== "Length") as (keyof Omit<GeneratorOptions, "Length">)[];
|
||||
|
||||
if (e.name === "include")
|
||||
keys = keys.filter(i => !i.startsWith("Exclude"));
|
||||
else
|
||||
keys = keys.filter(i => i.startsWith("Exclude"));
|
||||
|
||||
for (const key of keys)
|
||||
opts[key] = e.checkedItems.includes(key);
|
||||
|
||||
setOptions(opts);
|
||||
}
|
||||
|
||||
useEffect(resetOptions, [generatorOptions]);
|
||||
useEffect(RefreshPassword, [generatorOptions, quickOpts]);
|
||||
useEffect(refreshTimer.trigger, [password]);
|
||||
|
||||
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={ loc("generator@copy") } relationship="label">
|
||||
<fui.Button
|
||||
appearance="subtle" onClick={ CopyPassword }
|
||||
icon={
|
||||
copyTimer.isActive ?
|
||||
<Icons.CheckmarkRegular className={ cls.copyIcon } /> :
|
||||
<Icons.CopyRegular className={ cls.copyIcon } />
|
||||
} />
|
||||
</fui.Tooltip>
|
||||
|
||||
<fui.Tooltip content={ loc("generator@refresh") } relationship="label">
|
||||
<fui.Button
|
||||
appearance="subtle" onClick={ RefreshPassword }
|
||||
icon={
|
||||
<Icons.ArrowClockwiseRegular className={ fui.mergeClasses(refreshTimer.isActive && cls.refreshIcon) } />
|
||||
} />
|
||||
</fui.Tooltip>
|
||||
</> } />
|
||||
}
|
||||
|
||||
{ !props.collapse &&
|
||||
<div className={ cls.options }>
|
||||
<fui.InfoLabel info={ loc("generator@quickOptions__hint") }>
|
||||
{ loc("generator@quickOptions") }
|
||||
</fui.InfoLabel>
|
||||
|
||||
<div className={ cls.lengthContainer }>
|
||||
<fui.Slider
|
||||
min={ extOptions.MinLength } max={ Math.max(extOptions.MaxLength, generatorOptions.Length) }
|
||||
value={ quickOpts.Length } onChange={ (_, e) => setOptions({ Length: e.value }) } />
|
||||
<fui.Text>{ quickOpts.Length }</fui.Text>
|
||||
</div>
|
||||
|
||||
<div className={ cls.characterOptionsContainer }>
|
||||
<fui.Menu
|
||||
positioning="after" hasCheckmarks
|
||||
checkedValues={ { include: checkedOptions } }
|
||||
onCheckedValueChange={ OnCheckedValueChange }>
|
||||
|
||||
<fui.MenuTrigger disableButtonEnhancement>
|
||||
<fui.MenuButton appearance="subtle" icon={ <IncludeIcon /> }>{ loc("generator@include") }</fui.MenuButton>
|
||||
</fui.MenuTrigger>
|
||||
|
||||
<fui.MenuPopover>
|
||||
<fui.MenuList>
|
||||
<fui.MenuItemCheckbox name="include" value="Uppercase" icon={ <Icons.TextCaseUppercaseRegular /> }>
|
||||
{ loc("settings@uppercase") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="include" value="Lowercase" icon={ <Icons.TextCaseLowercaseRegular /> }>
|
||||
{ loc("settings@lowercase") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="include" value="Numeric" icon={ <Icons.NumberSymbolRegular /> }>
|
||||
{ loc("settings@numeric") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="include" value="Special" icon={ <Icons.MathSymbolsRegular /> }>
|
||||
{ loc("settings@special") }
|
||||
</fui.MenuItemCheckbox>
|
||||
</fui.MenuList>
|
||||
</fui.MenuPopover>
|
||||
</fui.Menu>
|
||||
|
||||
<fui.Menu
|
||||
positioning="before"
|
||||
checkedValues={ { exclude: checkedOptions } }
|
||||
onCheckedValueChange={ OnCheckedValueChange }>
|
||||
|
||||
<fui.MenuTrigger disableButtonEnhancement>
|
||||
<fui.MenuButton appearance="subtle" icon={ <ExcludeIcon /> }>{ loc("generator@exclude") }</fui.MenuButton>
|
||||
</fui.MenuTrigger>
|
||||
|
||||
<fui.MenuPopover>
|
||||
<fui.MenuList>
|
||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeSimilar">
|
||||
{ loc("settings@similar") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeAmbiguous" disabled={ !quickOpts.Special }>
|
||||
{ loc("settings@ambiguous") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="exclude" value="ExcludeRepeating">
|
||||
{ loc("settings@repeating") }
|
||||
</fui.MenuItemCheckbox>
|
||||
</fui.MenuList>
|
||||
</fui.MenuPopover>
|
||||
</fui.Menu>
|
||||
|
||||
<fui.Tooltip content={ loc("generator@reset") } relationship="label">
|
||||
<fui.Button appearance="subtle" icon={ <Icons.ArrowUndoRegular /> } onClick={ resetOptions } />
|
||||
</fui.Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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,122 +0,0 @@
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { ArrowUndoRegular, SettingsRegular } from "@fluentui/react-icons";
|
||||
import ExtensionOptions from "../Models/ExtensionOptions";
|
||||
import GeneratorOptions from "../Models/GeneratorOptions";
|
||||
import { GetLocaleString as loc } from "../Utils/Localization";
|
||||
import { CharacterHints } from "../Utils/PasswordGenerator";
|
||||
import { useStorage } from "../Utils/Storage";
|
||||
import { useStyles } from "./SettingsSection.styles";
|
||||
|
||||
// FIXME: Remove ts-ignore comments once slots override fix is released
|
||||
// Tracker: https://github.com/microsoft/fluentui/issues/27090
|
||||
|
||||
export default function SettingsSection(): JSX.Element
|
||||
{
|
||||
const { extOptions, generatorOptions, updateStorage } = useStorage();
|
||||
const cls = useStyles();
|
||||
|
||||
const infoLabel = (content: string, hint: string) => ({
|
||||
children: (_: unknown, slotProps: fui.LabelProps) => (
|
||||
<fui.InfoLabel { ...slotProps } info={ hint }>{ content }</fui.InfoLabel>
|
||||
)
|
||||
});
|
||||
|
||||
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)
|
||||
{
|
||||
updateStorage({ [key]: defaultValue });
|
||||
return;
|
||||
}
|
||||
|
||||
const value = parseInt(e.value);
|
||||
|
||||
if (!isNaN(value) && value >= 0)
|
||||
updateStorage({ [key]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<fui.AccordionItem value="settings">
|
||||
<fui.AccordionHeader as="h2" icon={ <SettingsRegular /> }>{ loc("settings@title") }</fui.AccordionHeader>
|
||||
|
||||
<fui.AccordionPanel className={ cls.root }>
|
||||
|
||||
<fui.Field label={ loc("settings@length") } hint={ loc("settings@length__hint") }>
|
||||
<fui.Input
|
||||
value={ generatorOptions.Length.toString() }
|
||||
onChange={ updateNumberField("Length", 0) } />
|
||||
</fui.Field>
|
||||
|
||||
<fui.Field label={ loc("settings@lengthRange") }>
|
||||
<div className={ cls.rangeContainer }>
|
||||
<fui.Input
|
||||
input={ { className: cls.rangeInput } }
|
||||
value={ extOptions.MinLength.toString() }
|
||||
onChange={ updateNumberField("MinLength", 4) } />
|
||||
|
||||
<fui.Divider />
|
||||
|
||||
<fui.Input
|
||||
input={ { className: cls.rangeInput } }
|
||||
value={ extOptions.MaxLength.toString() }
|
||||
onChange={ updateNumberField("MaxLength", 32) } />
|
||||
|
||||
<fui.Tooltip relationship="label" content={ loc("generator@reset") }>
|
||||
<fui.Button
|
||||
appearance="subtle" icon={ <ArrowUndoRegular /> }
|
||||
onClick={ () => updateStorage({ MinLength: 6, MaxLength: 32 }) } />
|
||||
</fui.Tooltip>
|
||||
</div>
|
||||
</fui.Field>
|
||||
|
||||
<fui.Divider />
|
||||
|
||||
<fui.Text>{ loc("settings@include") }</fui.Text>
|
||||
<div className={ cls.checkboxContainer }>
|
||||
<fui.Checkbox label={ loc("settings@uppercase") }
|
||||
checked={ generatorOptions.Uppercase }
|
||||
onChange={ setOption("Uppercase") } />
|
||||
<fui.Checkbox
|
||||
label={ loc("settings@lowercase") }
|
||||
checked={ generatorOptions.Lowercase }
|
||||
onChange={ setOption("Lowercase") } />
|
||||
<fui.Checkbox
|
||||
label={ loc("settings@numeric") }
|
||||
checked={ generatorOptions.Numeric }
|
||||
onChange={ setOption("Numeric") } />
|
||||
<fui.Checkbox
|
||||
label={ loc("settings@special") }
|
||||
checked={ generatorOptions.Special }
|
||||
onChange={ setOption("Special") } />
|
||||
</div>
|
||||
|
||||
<fui.Text>{ loc("settings@exclude") }</fui.Text>
|
||||
<div className={ cls.checkboxContainer }>
|
||||
<fui.Checkbox
|
||||
// @ts-expect-error See line 11
|
||||
label={ infoLabel(loc("settings@similar"), CharacterHints.Similar) }
|
||||
checked={ generatorOptions.ExcludeSimilar }
|
||||
onChange={ setOption("ExcludeSimilar") } />
|
||||
<fui.Checkbox
|
||||
// @ts-expect-error See line 11
|
||||
label={ infoLabel(loc("settings@ambiguous"), CharacterHints.Ambiguous) }
|
||||
disabled={ !generatorOptions.Special }
|
||||
checked={ generatorOptions.ExcludeAmbiguous }
|
||||
onChange={ setOption("ExcludeAmbiguous") } />
|
||||
<fui.Checkbox
|
||||
// @ts-expect-error See line 11
|
||||
label={ infoLabel(loc("settings@repeating"), loc("settings@repeating__hint")) }
|
||||
checked={ generatorOptions.ExcludeRepeating }
|
||||
onChange={ setOption("ExcludeRepeating") } />
|
||||
</div>
|
||||
|
||||
</fui.AccordionPanel>
|
||||
|
||||
</fui.AccordionItem>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import browser, { Manifest } from "webextension-polyfill";
|
||||
|
||||
export const PersonalLink =
|
||||
{
|
||||
Website: "https://xfox111.net",
|
||||
Twitter: "https://twitter.com/xfox111",
|
||||
BuyMeACoffee: "https://buymeacoffee.com/xfox111"
|
||||
};
|
||||
|
||||
export const WebstoreLink =
|
||||
{
|
||||
Chrome: "https://chrome.google.com/webstore/detail/password-generator/jnjobgjobffgmgfnkpkjfjkkfhfikmfl",
|
||||
Edge: "https://microsoftedge.microsoft.com/addons/detail/password-generator/manimdhobjbkfpeeehlhhneookiokpbj",
|
||||
Firefox: "https://addons.mozilla.org/firefox/addon/easy-password-generator"
|
||||
};
|
||||
|
||||
const getGithub = (path?: string): string =>
|
||||
`https://github.com/xfox111/PasswordGeneratorExtension${path ?? ""}`;
|
||||
|
||||
export const GithubLink =
|
||||
{
|
||||
Repository: getGithub(),
|
||||
Changelog: getGithub("/releases/latest"),
|
||||
TranslationGuide: getGithub("/wiki/Contribution-Guidelines#contributing-to-translations"),
|
||||
License: getGithub("/blob/main/LICENSE")
|
||||
};
|
||||
|
||||
export const GetFeedbackLink = () =>
|
||||
{
|
||||
if (__BROWSER__ === "firefox")
|
||||
return WebstoreLink.Firefox;
|
||||
|
||||
const manifest: Manifest.WebExtensionManifest = browser.runtime.getManifest();
|
||||
|
||||
const updateUrl: URL = new URL((manifest as unknown as Record<string, unknown>).update_url as string ?? "about:blank");
|
||||
|
||||
if (updateUrl.host === "edge.microsoft.com")
|
||||
return WebstoreLink.Edge;
|
||||
if (updateUrl.host === "clients2.google.com")
|
||||
return WebstoreLink.Chrome;
|
||||
|
||||
return "mailto:feedback@xfox111.net";
|
||||
};
|
||||
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"name": {
|
||||
"message": "Password Generator",
|
||||
"description": "Extension name"
|
||||
},
|
||||
"description": {
|
||||
"message": "Password generator extension allows you to easily generate long and secure password in one click",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"author": {
|
||||
"message": "Eugene Fox",
|
||||
"description": "Extension author"
|
||||
},
|
||||
"error@moreCharacters": {
|
||||
"message": "Password length must be at least 4 characters",
|
||||
"description": "Error message when password length is less than 4 characters"
|
||||
},
|
||||
"error@noCharacters": {
|
||||
"message": "At least one set of characters must be selected",
|
||||
"description": "Error message when no character set is selected"
|
||||
},
|
||||
"error@tooLong": {
|
||||
"message": "Length is too long to generate password with unique characters",
|
||||
"description": "Error message when password length is too long to generate password with unique characters"
|
||||
},
|
||||
"about@title":
|
||||
{
|
||||
"message": "About",
|
||||
"description": "About section title"
|
||||
},
|
||||
"about@developedBy":
|
||||
{
|
||||
"message": "Developed by Eugene Fox",
|
||||
"description": "Developer credits in about section"
|
||||
},
|
||||
"about@licensedUnder":
|
||||
{
|
||||
"message": "Licensed under",
|
||||
"description": "License info in about section"
|
||||
},
|
||||
"about@mitLicense":
|
||||
{
|
||||
"message": "MIT License",
|
||||
"description": "License name"
|
||||
},
|
||||
"about@translationCta":
|
||||
{
|
||||
"message": "Found a typo or want a translation for your language?",
|
||||
"description": "Translation CTA in about section"
|
||||
},
|
||||
"about@translationCtaButton":
|
||||
{
|
||||
"message": "Get started here",
|
||||
"description": "Translation CTA button in about section"
|
||||
},
|
||||
"about@website":
|
||||
{
|
||||
"message": "My website",
|
||||
"description": "Website link in about section"
|
||||
},
|
||||
"about@sourceCode":
|
||||
{
|
||||
"message": "Source code",
|
||||
"description": "Source code link in about section"
|
||||
},
|
||||
"about@changelog":
|
||||
{
|
||||
"message": "Changelog",
|
||||
"description": "Changelog link in about section"
|
||||
},
|
||||
"about@feedback":
|
||||
{
|
||||
"message": "Leave feedback",
|
||||
"description": "Feedback link in about section"
|
||||
},
|
||||
"about@sponsor":
|
||||
{
|
||||
"message": "Buy me a coffee",
|
||||
"description": "Buy me a coffee donation link in about section"
|
||||
},
|
||||
"settings@title":
|
||||
{
|
||||
"message": "Settings",
|
||||
"description": "Settings section title"
|
||||
},
|
||||
"settings@length":
|
||||
{
|
||||
"message": "Default password length",
|
||||
"description": "Password length label in settings section"
|
||||
},
|
||||
"settings@length__hint":
|
||||
{
|
||||
"message": "Recommended length: 8–16",
|
||||
"description": "Password length recommendatin in settings section"
|
||||
},
|
||||
"settings@lengthRange":
|
||||
{
|
||||
"message": "Quick adjustment length range",
|
||||
"description": "Quick adjustment length range label in settings section"
|
||||
},
|
||||
"settings@include":
|
||||
{
|
||||
"message": "Include symbols",
|
||||
"description": "Include character sets section"
|
||||
},
|
||||
"settings@uppercase":
|
||||
{
|
||||
"message": "Uppercase",
|
||||
"description": "Uppercase character set label in settings section"
|
||||
},
|
||||
"settings@lowercase":
|
||||
{
|
||||
"message": "Lowercase",
|
||||
"description": "Lowercase character set label in settings section"
|
||||
},
|
||||
"settings@numeric":
|
||||
{
|
||||
"message": "Numeric",
|
||||
"description": "Numbers character set label in settings section"
|
||||
},
|
||||
"settings@special":
|
||||
{
|
||||
"message": "Special",
|
||||
"description": "Special characters character set label in settings section"
|
||||
},
|
||||
"settings@exclude":
|
||||
{
|
||||
"message": "Exclude symbols",
|
||||
"description": "Exclude character sets section"
|
||||
},
|
||||
"settings@similar":
|
||||
{
|
||||
"message": "Similar",
|
||||
"description": "Similar characters character set label in settings section"
|
||||
},
|
||||
"settings@ambiguous":
|
||||
{
|
||||
"message": "Ambiguous",
|
||||
"description": "Ambiguous characters character set label in settings section"
|
||||
},
|
||||
"settings@repeating":
|
||||
{
|
||||
"message": "Repeating",
|
||||
"description": "Repeating characters character set label in settings section"
|
||||
},
|
||||
"settings@repeating__hint":
|
||||
{
|
||||
"message": "Each character in the password will be unique",
|
||||
"description": "Repeating characters character set hint in settings section"
|
||||
},
|
||||
"generator@copy":
|
||||
{
|
||||
"message": "Copy",
|
||||
"description": "Copy button label"
|
||||
},
|
||||
"generator@reset":
|
||||
{
|
||||
"message": "Reset",
|
||||
"description": "Reset button label"
|
||||
},
|
||||
"generator@refresh":
|
||||
{
|
||||
"message": "Generate new",
|
||||
"description": "Generate new password button label"
|
||||
},
|
||||
"generator@quickOptions":
|
||||
{
|
||||
"message": "Quick adjustments",
|
||||
"description": "Quick options section title"
|
||||
},
|
||||
"generator@quickOptions__hint":
|
||||
{
|
||||
"message": "These adjustments will not be saved",
|
||||
"description": "Quick options hint"
|
||||
},
|
||||
"generator@include":
|
||||
{
|
||||
"message": "Include",
|
||||
"description": "Include characters button label"
|
||||
},
|
||||
"generator@exclude":
|
||||
{
|
||||
"message": "Exclude",
|
||||
"description": "Exclude characters button label"
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"name": {
|
||||
"message": "Generator haseł",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"description": {
|
||||
"message": "Rozszerzenie, które pozwala na łatwe generowanie trudnych i bezpiecznych haseł w jednym kliknięciu",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"author": {
|
||||
"message": "Jewgienij Lis",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"error@moreCharacters": {
|
||||
"message": "Minimalna długość hasła to 4 znaki",
|
||||
"description": "Error message when password length is less than 4 characters"
|
||||
},
|
||||
"error@noCharacters": {
|
||||
"message": "Musisz wybrać przynajmniej jeden rodzaj znaków",
|
||||
"description": "Error message when no character set is selected"
|
||||
},
|
||||
"error@tooLong": {
|
||||
"message": "Wybrana długość jest zbyt duża, aby wygenerować unikalne znaki",
|
||||
"description": "Error message when password length is too long to generate password with unique characters"
|
||||
},
|
||||
"about@title":
|
||||
{
|
||||
"message": "O rozszerzeniu",
|
||||
"description": "About section title"
|
||||
},
|
||||
"about@developedBy":
|
||||
{
|
||||
"message": "Autor: Jewgienij Lis",
|
||||
"description": "Developer credits in about section"
|
||||
},
|
||||
"about@licensedUnder":
|
||||
{
|
||||
"message": "",
|
||||
"description": "License info in about section"
|
||||
},
|
||||
"about@mitLicense":
|
||||
{
|
||||
"message": "Licencja MIT",
|
||||
"description": "License name"
|
||||
},
|
||||
"about@translationCta":
|
||||
{
|
||||
"message": "Znalazłeś błąd lub potrzebujesz tłumaczenia na swój język?",
|
||||
"description": "Translation CTA in about section"
|
||||
},
|
||||
"about@translationCtaButton":
|
||||
{
|
||||
"message": "Zacznij tutaj",
|
||||
"description": "Translation CTA button in about section"
|
||||
},
|
||||
"about@website":
|
||||
{
|
||||
"message": "Moja strona internetowa",
|
||||
"description": "Website link in about section"
|
||||
},
|
||||
"about@sourceCode":
|
||||
{
|
||||
"message": "Kod źródłowy",
|
||||
"description": "Source code link in about section"
|
||||
},
|
||||
"about@changelog":
|
||||
{
|
||||
"message": "Lista zmian",
|
||||
"description": "Changelog link in about section"
|
||||
},
|
||||
"about@feedback":
|
||||
{
|
||||
"message": "Zostaw opinię",
|
||||
"description": "Feedback link in about section"
|
||||
},
|
||||
"about@sponsor":
|
||||
{
|
||||
"message": "Wesprzyj",
|
||||
"description": "Buy me a coffee donation link in about section"
|
||||
},
|
||||
"settings@title":
|
||||
{
|
||||
"message": "Ustawienia",
|
||||
"description": "Settings section title"
|
||||
},
|
||||
"settings@length":
|
||||
{
|
||||
"message": "Domyślna długość hasła",
|
||||
"description": "Password length label in settings section"
|
||||
},
|
||||
"settings@length__hint":
|
||||
{
|
||||
"message": "Zalecana długość: 8–16",
|
||||
"description": "Password length recommendatin in settings section"
|
||||
},
|
||||
"settings@lengthRange":
|
||||
{
|
||||
"message": "Zakres długości dla szybkich ustawień",
|
||||
"description": "Quick adjustment length range label in settings section"
|
||||
},
|
||||
"settings@include":
|
||||
{
|
||||
"message": "Dodaj znaki",
|
||||
"description": "Include character sets section"
|
||||
},
|
||||
"settings@uppercase":
|
||||
{
|
||||
"message": "Wielkie",
|
||||
"description": "Uppercase character set label in settings section"
|
||||
},
|
||||
"settings@lowercase":
|
||||
{
|
||||
"message": "Małe",
|
||||
"description": "Lowercase character set label in settings section"
|
||||
},
|
||||
"settings@numeric":
|
||||
{
|
||||
"message": "Cyfry",
|
||||
"description": "Numbers character set label in settings section"
|
||||
},
|
||||
"settings@special":
|
||||
{
|
||||
"message": "Specjalne",
|
||||
"description": "Special characters character set label in settings section"
|
||||
},
|
||||
"settings@exclude":
|
||||
{
|
||||
"message": "Wyklucz znaki",
|
||||
"description": "Exclude character sets section"
|
||||
},
|
||||
"settings@similar":
|
||||
{
|
||||
"message": "Podobne",
|
||||
"description": "Similar characters character set label in settings section"
|
||||
},
|
||||
"settings@ambiguous":
|
||||
{
|
||||
"message": "Niebezpieczne",
|
||||
"description": "Ambiguous characters character set label in settings section"
|
||||
},
|
||||
"settings@repeating":
|
||||
{
|
||||
"message": "Powtarzające się",
|
||||
"description": "Repeating characters character set label in settings section"
|
||||
},
|
||||
"settings@repeating__hint":
|
||||
{
|
||||
"message": "Każdy znak hasła będzie unikalny",
|
||||
"description": "Repeating characters character set hint in settings section"
|
||||
},
|
||||
"generator@copy":
|
||||
{
|
||||
"message": "Kopiuj",
|
||||
"description": "Copy button label"
|
||||
},
|
||||
"generator@reset":
|
||||
{
|
||||
"message": "Resetuj",
|
||||
"description": "Reset button label"
|
||||
},
|
||||
"generator@refresh":
|
||||
{
|
||||
"message": "Wygeneruj nowe",
|
||||
"description": "Generate new password button label"
|
||||
},
|
||||
"generator@quickOptions":
|
||||
{
|
||||
"message": "Szybkie ustawienia",
|
||||
"description": "Quick options section title"
|
||||
},
|
||||
"generator@quickOptions__hint":
|
||||
{
|
||||
"message": "Te ustawienia nie zostaną zapisane",
|
||||
"description": "Quick options hint"
|
||||
},
|
||||
"generator@include":
|
||||
{
|
||||
"message": "Dodaj",
|
||||
"description": "Include characters button label"
|
||||
},
|
||||
"generator@exclude":
|
||||
{
|
||||
"message": "Wyklucz",
|
||||
"description": "Exclude characters button label"
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"name": {
|
||||
"message": "Gerador de Senhas",
|
||||
"description": "Extension name"
|
||||
},
|
||||
"description": {
|
||||
"message": "A extensão do gerador de senhas permite gerar facilmente uma senha longa e segura em um clique",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"author": {
|
||||
"message": "Eugênio Fox",
|
||||
"description": "Extension author"
|
||||
},
|
||||
"error@moreCharacters": {
|
||||
"message": "O comprimento da senha deve ter pelo menos 4 caracteres",
|
||||
"description": "Error message when password length is less than 4 characters"
|
||||
},
|
||||
"error@noCharacters": {
|
||||
"message": "É necessário selecionar pelo menos um conjunto de caracteres",
|
||||
"description": "Error message when no character set is selected"
|
||||
},
|
||||
"error@tooLong": {
|
||||
"message": "O comprimento é muito longo para gerar uma senha com caracteres únicos",
|
||||
"description": "Error message when password length is too long to generate password with unique characters"
|
||||
},
|
||||
"about@title":
|
||||
{
|
||||
"message": "Sobre",
|
||||
"description": "About section title"
|
||||
},
|
||||
"about@developedBy":
|
||||
{
|
||||
"message": "Desenvolvido por Eugênio Fox",
|
||||
"description": "Developer credits in about section"
|
||||
},
|
||||
"about@licensedUnder":
|
||||
{
|
||||
"message": "Licenciado sob",
|
||||
"description": "License info in about section"
|
||||
},
|
||||
"about@mitLicense":
|
||||
{
|
||||
"message": "Licença MIT",
|
||||
"description": "License name"
|
||||
},
|
||||
"about@translationCta":
|
||||
{
|
||||
"message": "Encontrou um erro ou quer uma tradução no seu idioma?",
|
||||
"description": "Translation CTA in about section"
|
||||
},
|
||||
"about@translationCtaButton":
|
||||
{
|
||||
"message": "Comece aqui",
|
||||
"description": "Translation CTA button in about section"
|
||||
},
|
||||
"about@website":
|
||||
{
|
||||
"message": "Meu site",
|
||||
"description": "Website link in about section"
|
||||
},
|
||||
"about@sourceCode":
|
||||
{
|
||||
"message": "Código fonte",
|
||||
"description": "Source code link in about section"
|
||||
},
|
||||
"about@changelog":
|
||||
{
|
||||
"message": "Registro de alterações",
|
||||
"description": "Changelog link in about section"
|
||||
},
|
||||
"about@feedback":
|
||||
{
|
||||
"message": "Enviar comentários",
|
||||
"description": "Feedback link in about section"
|
||||
},
|
||||
"about@sponsor":
|
||||
{
|
||||
"message": "Pague um café",
|
||||
"description": "Buy me a coffee donation link in about section"
|
||||
},
|
||||
"settings@title":
|
||||
{
|
||||
"message": "Configurações",
|
||||
"description": "Settings section title"
|
||||
},
|
||||
"settings@length":
|
||||
{
|
||||
"message": "Comprimento padrão da senha",
|
||||
"description": "Password length label in settings section"
|
||||
},
|
||||
"settings@length__hint":
|
||||
{
|
||||
"message": "Comprimento recomendado: 8–16",
|
||||
"description": "Password length recommendatin in settings section"
|
||||
},
|
||||
"settings@lengthRange":
|
||||
{
|
||||
"message": "Intervalo do comprimento de ajuste rápido",
|
||||
"description": "Quick adjustment length range label in settings section"
|
||||
},
|
||||
"settings@include":
|
||||
{
|
||||
"message": "Incluir símbolos",
|
||||
"description": "Include character sets section"
|
||||
},
|
||||
"settings@uppercase":
|
||||
{
|
||||
"message": "Maiúsculos",
|
||||
"description": "Uppercase character set label in settings section"
|
||||
},
|
||||
"settings@lowercase":
|
||||
{
|
||||
"message": "Minúsculos",
|
||||
"description": "Lowercase character set label in settings section"
|
||||
},
|
||||
"settings@numeric":
|
||||
{
|
||||
"message": "Numéricos",
|
||||
"description": "Numbers character set label in settings section"
|
||||
},
|
||||
"settings@special":
|
||||
{
|
||||
"message": "Especiais",
|
||||
"description": "Special characters character set label in settings section"
|
||||
},
|
||||
"settings@exclude":
|
||||
{
|
||||
"message": "Excluir símbolos",
|
||||
"description": "Exclude character sets section"
|
||||
},
|
||||
"settings@similar":
|
||||
{
|
||||
"message": "Semelhantes",
|
||||
"description": "Similar characters character set label in settings section"
|
||||
},
|
||||
"settings@ambiguous":
|
||||
{
|
||||
"message": "Ambíguos",
|
||||
"description": "Ambiguous characters character set label in settings section"
|
||||
},
|
||||
"settings@repeating":
|
||||
{
|
||||
"message": "Repetidos",
|
||||
"description": "Repeating characters character set label in settings section"
|
||||
},
|
||||
"settings@repeating__hint":
|
||||
{
|
||||
"message": "Cada caractere na senha será único",
|
||||
"description": "Repeating characters character set hint in settings section"
|
||||
},
|
||||
"generator@copy":
|
||||
{
|
||||
"message": "Copiar",
|
||||
"description": "Copy button label"
|
||||
},
|
||||
"generator@reset":
|
||||
{
|
||||
"message": "Redefinir",
|
||||
"description": "Reset button label"
|
||||
},
|
||||
"generator@refresh":
|
||||
{
|
||||
"message": "Gerar nova",
|
||||
"description": "Generate new password button label"
|
||||
},
|
||||
"generator@quickOptions":
|
||||
{
|
||||
"message": "Ajustes rápidos",
|
||||
"description": "Quick options section title"
|
||||
},
|
||||
"generator@quickOptions__hint":
|
||||
{
|
||||
"message": "Esses ajustes não serão salvos",
|
||||
"description": "Quick options hint"
|
||||
},
|
||||
"generator@include":
|
||||
{
|
||||
"message": "Incluir",
|
||||
"description": "Include characters button label"
|
||||
},
|
||||
"generator@exclude":
|
||||
{
|
||||
"message": "Excluir",
|
||||
"description": "Exclude characters button label"
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"name": {
|
||||
"message": "Генератор паролей",
|
||||
"description": "Extension name"
|
||||
},
|
||||
"description": {
|
||||
"message": "Расширение, позволяющее легко генерировать сложные и надежные пароли в один клик",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"author": {
|
||||
"message": "Евгений Лис",
|
||||
"description": "Extension author"
|
||||
},
|
||||
"error@moreCharacters": {
|
||||
"message": "Минимальная длина пароля — 4 символа",
|
||||
"description": "Error message when password length is less than 4 characters"
|
||||
},
|
||||
"error@noCharacters": {
|
||||
"message": "Необходимо выбрать хотя бы один вид символов",
|
||||
"description": "Error message when no character set is selected"
|
||||
},
|
||||
"error@tooLong": {
|
||||
"message": "Выбранная длина слишком большая, для генерации уникальных символов",
|
||||
"description": "Error message when password length is too long to generate password with unique characters"
|
||||
},
|
||||
"about@title":
|
||||
{
|
||||
"message": "О расширении",
|
||||
"description": "About section title"
|
||||
},
|
||||
"about@developedBy":
|
||||
{
|
||||
"message": "Разработчк: Евгений Лис",
|
||||
"description": "Developer credits in about section"
|
||||
},
|
||||
"about@licensedUnder":
|
||||
{
|
||||
"message": "",
|
||||
"description": "License info in about section"
|
||||
},
|
||||
"about@mitLicense":
|
||||
{
|
||||
"message": "Лицензия MIT",
|
||||
"description": "License name"
|
||||
},
|
||||
"about@translationCta":
|
||||
{
|
||||
"message": "Нашли опечатку или хотите перевод на ваш язык?",
|
||||
"description": "Translation CTA in about section"
|
||||
},
|
||||
"about@translationCtaButton":
|
||||
{
|
||||
"message": "Начните здесь",
|
||||
"description": "Translation CTA button in about section"
|
||||
},
|
||||
"about@website":
|
||||
{
|
||||
"message": "Мой веб-сайт",
|
||||
"description": "Website link in about section"
|
||||
},
|
||||
"about@sourceCode":
|
||||
{
|
||||
"message": "Исходный код",
|
||||
"description": "Source code link in about section"
|
||||
},
|
||||
"about@changelog":
|
||||
{
|
||||
"message": "Список изменений",
|
||||
"description": "Changelog link in about section"
|
||||
},
|
||||
"about@feedback":
|
||||
{
|
||||
"message": "Оставить отзыв",
|
||||
"description": "Feedback link in about section"
|
||||
},
|
||||
"about@sponsor":
|
||||
{
|
||||
"message": "Поддержать",
|
||||
"description": "Buy me a coffee donation link in about section"
|
||||
},
|
||||
"settings@title":
|
||||
{
|
||||
"message": "Настройки",
|
||||
"description": "Settings section title"
|
||||
},
|
||||
"settings@length":
|
||||
{
|
||||
"message": "Длина пароля по умолчанию",
|
||||
"description": "Password length label in settings section"
|
||||
},
|
||||
"settings@length__hint":
|
||||
{
|
||||
"message": "Рекомендованная длина: 8–16",
|
||||
"description": "Password length recommendatin in settings section"
|
||||
},
|
||||
"settings@lengthRange":
|
||||
{
|
||||
"message": "Диапазон длины для быстрых настроек",
|
||||
"description": "Quick adjustment length range label in settings section"
|
||||
},
|
||||
"settings@include":
|
||||
{
|
||||
"message": "Добавить символы",
|
||||
"description": "Include character sets section"
|
||||
},
|
||||
"settings@uppercase":
|
||||
{
|
||||
"message": "Прописные",
|
||||
"description": "Uppercase character set label in settings section"
|
||||
},
|
||||
"settings@lowercase":
|
||||
{
|
||||
"message": "Строчные",
|
||||
"description": "Lowercase character set label in settings section"
|
||||
},
|
||||
"settings@numeric":
|
||||
{
|
||||
"message": "Числовые",
|
||||
"description": "Numbers character set label in settings section"
|
||||
},
|
||||
"settings@special":
|
||||
{
|
||||
"message": "Специальные",
|
||||
"description": "Special characters character set label in settings section"
|
||||
},
|
||||
"settings@exclude":
|
||||
{
|
||||
"message": "Исключить символы",
|
||||
"description": "Exclude character sets section"
|
||||
},
|
||||
"settings@similar":
|
||||
{
|
||||
"message": "Похожие",
|
||||
"description": "Similar characters character set label in settings section"
|
||||
},
|
||||
"settings@ambiguous":
|
||||
{
|
||||
"message": "Особые",
|
||||
"description": "Ambiguous characters character set label in settings section"
|
||||
},
|
||||
"settings@repeating":
|
||||
{
|
||||
"message": "Повторяющиеся",
|
||||
"description": "Repeating characters character set label in settings section"
|
||||
},
|
||||
"settings@repeating__hint":
|
||||
{
|
||||
"message": "Каждый символ пароля будет уникальным",
|
||||
"description": "Repeating characters character set hint in settings section"
|
||||
},
|
||||
"generator@copy":
|
||||
{
|
||||
"message": "Копировать",
|
||||
"description": "Copy button label"
|
||||
},
|
||||
"generator@reset":
|
||||
{
|
||||
"message": "Сбросить",
|
||||
"description": "Reset button label"
|
||||
},
|
||||
"generator@refresh":
|
||||
{
|
||||
"message": "Сгенерировать новый",
|
||||
"description": "Generate new password button label"
|
||||
},
|
||||
"generator@quickOptions":
|
||||
{
|
||||
"message": "Быстрые настройки",
|
||||
"description": "Quick options section title"
|
||||
},
|
||||
"generator@quickOptions__hint":
|
||||
{
|
||||
"message": "Эти настройки не будут сохранены",
|
||||
"description": "Quick options hint"
|
||||
},
|
||||
"generator@include":
|
||||
{
|
||||
"message": "Добавить",
|
||||
"description": "Include characters button label"
|
||||
},
|
||||
"generator@exclude":
|
||||
{
|
||||
"message": "Исключить",
|
||||
"description": "Exclude characters button label"
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"name": {
|
||||
"message": "Генератор паролів",
|
||||
"description": "Extension name"
|
||||
},
|
||||
"description": {
|
||||
"message": "Розширення, яке дозволяє легко генерувати складні та надійні паролі в один клік",
|
||||
"description": "Extension description"
|
||||
},
|
||||
"author": {
|
||||
"message": "Євген Лис",
|
||||
"description": "Extension author"
|
||||
},
|
||||
"error@moreCharacters": {
|
||||
"message": "Мінімальна довжина пароля — 4 символи",
|
||||
"description": "Error message when password length is less than 4 characters"
|
||||
},
|
||||
"error@noCharacters": {
|
||||
"message": "Необхідно вибрати хоча б один вид символів",
|
||||
"description": "Error message when no character set is selected"
|
||||
},
|
||||
"error@tooLong": {
|
||||
"message": "Вибрана довжина занадто велика, для генерації унікальних символів",
|
||||
"description": "Error message when password length is too long to generate password with unique characters"
|
||||
},
|
||||
"about@title":
|
||||
{
|
||||
"message": "Про розширення",
|
||||
"description": "About section title"
|
||||
},
|
||||
"about@developedBy":
|
||||
{
|
||||
"message": "Розробник: Євген Лис",
|
||||
"description": "Developer credits in about section"
|
||||
},
|
||||
"about@licensedUnder":
|
||||
{
|
||||
"message": "",
|
||||
"description": "License info in about section"
|
||||
},
|
||||
"about@mitLicense":
|
||||
{
|
||||
"message": "Ліцензія MIT",
|
||||
"description": "License name"
|
||||
},
|
||||
"about@translationCta":
|
||||
{
|
||||
"message": "Знайшли помилку або хочете переклад на вашу мову?",
|
||||
"description": "Translation CTA in about section"
|
||||
},
|
||||
"about@translationCtaButton":
|
||||
{
|
||||
"message": "Почніть тут",
|
||||
"description": "Translation CTA button in about section"
|
||||
},
|
||||
"about@website":
|
||||
{
|
||||
"message": "Мій веб-сайт",
|
||||
"description": "Website link in about section"
|
||||
},
|
||||
"about@sourceCode":
|
||||
{
|
||||
"message": "Вихідний код",
|
||||
"description": "Source code link in about section"
|
||||
},
|
||||
"about@changelog":
|
||||
{
|
||||
"message": "Список змін",
|
||||
"description": "Changelog link in about section"
|
||||
},
|
||||
"about@feedback":
|
||||
{
|
||||
"message": "Залишити відгук",
|
||||
"description": "Feedback link in about section"
|
||||
},
|
||||
"about@sponsor":
|
||||
{
|
||||
"message": "Підтримати",
|
||||
"description": "Buy me a coffee donation link in about section"
|
||||
},
|
||||
"settings@title":
|
||||
{
|
||||
"message": "Налаштування",
|
||||
"description": "Settings section title"
|
||||
},
|
||||
"settings@length":
|
||||
{
|
||||
"message": "Довжина пароля за замовчуванням",
|
||||
"description": "Password length label in settings section"
|
||||
},
|
||||
"settings@length__hint":
|
||||
{
|
||||
"message": "Рекомендована довжина: 8–16",
|
||||
"description": "Password length recommendatin in settings section"
|
||||
},
|
||||
"settings@lengthRange":
|
||||
{
|
||||
"message": "Діапазон довжини для швидких налаштувань",
|
||||
"description": "Quick adjustment length range label in settings section"
|
||||
},
|
||||
"settings@include":
|
||||
{
|
||||
"message": "Додати символи",
|
||||
"description": "Include character sets section"
|
||||
},
|
||||
"settings@uppercase":
|
||||
{
|
||||
"message": "Великі",
|
||||
"description": "Uppercase character set label in settings section"
|
||||
},
|
||||
"settings@lowercase":
|
||||
{
|
||||
"message": "Малі",
|
||||
"description": "Lowercase character set label in settings section"
|
||||
},
|
||||
"settings@numeric":
|
||||
{
|
||||
"message": "Числові",
|
||||
"description": "Numbers character set label in settings section"
|
||||
},
|
||||
"settings@special":
|
||||
{
|
||||
"message": "Спеціальні",
|
||||
"description": "Special characters character set label in settings section"
|
||||
},
|
||||
"settings@exclude":
|
||||
{
|
||||
"message": "Виключити символи",
|
||||
"description": "Exclude character sets section"
|
||||
},
|
||||
"settings@similar":
|
||||
{
|
||||
"message": "Схожі",
|
||||
"description": "Similar characters character set label in settings section"
|
||||
},
|
||||
"settings@ambiguous":
|
||||
{
|
||||
"message": "Особливі",
|
||||
"description": "Ambiguous characters character set label in settings section"
|
||||
},
|
||||
"settings@repeating":
|
||||
{
|
||||
"message": "Повторювані",
|
||||
"description": "Repeating characters character set label in settings section"
|
||||
},
|
||||
"settings@repeating__hint":
|
||||
{
|
||||
"message": "Кожен символ пароля буде унікальним",
|
||||
"description": "Repeating characters character set hint in settings section"
|
||||
},
|
||||
"generator@copy":
|
||||
{
|
||||
"message": "Копіювати",
|
||||
"description": "Copy button label"
|
||||
},
|
||||
"generator@reset":
|
||||
{
|
||||
"message": "Скинути",
|
||||
"description": "Reset button label"
|
||||
},
|
||||
"generator@refresh":
|
||||
{
|
||||
"message": "Згенерувати новий",
|
||||
"description": "Generate new password button label"
|
||||
},
|
||||
"generator@quickOptions":
|
||||
{
|
||||
"message": "Швидкі налаштування",
|
||||
"description": "Quick options section title"
|
||||
},
|
||||
"generator@quickOptions__hint":
|
||||
{
|
||||
"message": "Ці налаштування не будуть збережені",
|
||||
"description": "Quick options hint"
|
||||
},
|
||||
"generator@include":
|
||||
{
|
||||
"message": "Додати",
|
||||
"description": "Include characters button label"
|
||||
},
|
||||
"generator@exclude":
|
||||
{
|
||||
"message": "Виключити",
|
||||
"description": "Exclude characters button label"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { mergeClasses } from "@fluentui/react-components";
|
||||
import { useStyles } from "./Snow.styles";
|
||||
|
||||
const SNOWFLAKES_NUM: number = 100;
|
||||
|
||||
export default function Snow(): JSX.Element
|
||||
{
|
||||
const cls = useStyles(SNOWFLAKES_NUM)();
|
||||
|
||||
if (![0, 11].includes(new Date().getMonth()))
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<div className={ cls.snow }>
|
||||
{ [...Array(SNOWFLAKES_NUM)].map((_, i) => <div key={ i } className={ mergeClasses(cls.snowflake, cls[`snowflake-${i}`]) } />) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import Snow from "./Snow";
|
||||
|
||||
const Specials = (): JSX.Element => <>
|
||||
<Snow />
|
||||
</>;
|
||||
|
||||
export default Specials;
|
||||
@@ -1,11 +0,0 @@
|
||||
import browser from "webextension-polyfill";
|
||||
import messages from "../Data/Locales/en/messages.json";
|
||||
|
||||
/**
|
||||
* Gets the localized string for the specified message. If the message is missing, this method returns an empty string (''). If the format of the call is wrong — for example, messageName is not a string or the substitutions array has more than 9 elements — this method returns .
|
||||
* @param key The name of the message, as specified in the file.
|
||||
* @param subustitutions Optional. Substitution strings, if the message requires any.
|
||||
* @returns Message localized for current locale.
|
||||
*/
|
||||
export const GetLocaleString = (key: keyof typeof messages, ...subustitutions: string[]): string =>
|
||||
browser.i18n.getMessage(key, subustitutions) ?? key;
|
||||
@@ -1,129 +0,0 @@
|
||||
import GeneratorOptions from "../Models/GeneratorOptions";
|
||||
import { GetLocaleString as loc } from "./Localization";
|
||||
|
||||
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(loc("error@moreCharacters"));
|
||||
|
||||
const availableCharacters: string = GetAvailableCharacters(options);
|
||||
|
||||
if (availableCharacters.length < 1)
|
||||
throw new Error(loc("error@noCharacters"));
|
||||
|
||||
if (options.ExcludeRepeating && options.Length > availableCharacters.length)
|
||||
throw new Error(loc("error@tooLong"));
|
||||
}
|
||||
|
||||
// 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("");
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
|
||||
"name": "__MSG_name__",
|
||||
"description": "__MSG_description__",
|
||||
"author": "__MSG_author__",
|
||||
"homepage_url": "https://github.com/xfox111/PasswordGeneratorExtension",
|
||||
|
||||
"default_locale": "en",
|
||||
|
||||
"icons": {
|
||||
"16": "icons/icon-16.png",
|
||||
"32": "icons/icon-32.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
|
||||
"action": {
|
||||
"default_popup": "src/popup.html"
|
||||
},
|
||||
|
||||
"{{firefox}}.browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "passwordgenerator@xfox111.net",
|
||||
"strict_min_version": "109.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
html,
|
||||
body
|
||||
{
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
p, ul, ol, li
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes scaleUpIn
|
||||
{
|
||||
from
|
||||
{
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to
|
||||
{
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin
|
||||
{
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn
|
||||
{
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes snowfall
|
||||
{
|
||||
0%
|
||||
{
|
||||
transform: translate3d(var(--left-ini), 0, 0);
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
100%
|
||||
{
|
||||
transform: translate3d(var(--left-end), 610px, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
|
||||
type SupportedBrowser = "chrome" | "firefox";
|
||||
declare const __BROWSER__: SupportedBrowser;
|
||||
@@ -1,36 +1,7 @@
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function RandomUpperCase(passphrase: string): string
|
||||
{
|
||||
const sequence: boolean[] = getBooleanSequence(passphrase.length);
|
||||
let result: string = "";
|
||||
|
||||
for (let i = 0; i < passphrase.length; i++)
|
||||
result += sequence[i] ? passphrase[i].toLocaleUpperCase() : passphrase[i];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function swapCharacters(passphrase: string): string
|
||||
{
|
||||
const sequence: boolean[] = getBooleanSequence(passphrase.length);
|
||||
let result: string = "";
|
||||
|
||||
for (let i = 0; i < passphrase.length; i++)
|
||||
if (sequence[i])
|
||||
switch (passphrase[i].toLocaleLowerCase())
|
||||
{
|
||||
case "a":
|
||||
result += getRandomInt(0, 100) < 50 ? "@" : "4";
|
||||
break;
|
||||
case "e":
|
||||
result += "3";
|
||||
break;
|
||||
case "i":
|
||||
result += "!";
|
||||
break;
|
||||
case "s":
|
||||
result += getRandomInt(0, 100) < 50 ? "$" : "5";
|
||||
break;
|
||||
default:
|
||||
result += passphrase[i];
|
||||
break;
|
||||
}
|
||||
else
|
||||
result += passphrase[i];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export type PassphraseProps =
|
||||
{
|
||||
wordCount: number;
|
||||
swapCharacters: boolean;
|
||||
randomizeCase: boolean;
|
||||
allowRepeating: boolean;
|
||||
separator: string;
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
import { pickRandomFromArray, shuffleString } from "./randomUtils";
|
||||
|
||||
const Characters =
|
||||
{
|
||||
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||
numeric: "1234567890",
|
||||
special: "!#$%&*+-=?@^_"
|
||||
};
|
||||
|
||||
const similar: string = "iIl1Lo0O";
|
||||
const ambiguous: string = "{}[]()/\\'\"`~,;:.<>";
|
||||
|
||||
export const CharacterHints = { ...Characters, similar, ambiguous };
|
||||
|
||||
/**
|
||||
* Generates a random password
|
||||
* @param options Options for password generation
|
||||
* @returns Randomly generated password
|
||||
* @throws Error if options are invalid
|
||||
*/
|
||||
export function generatePassword(options: PasswordProps): string
|
||||
{
|
||||
validateOptions(options);
|
||||
|
||||
let password: string = getRequiredCharacters(options);
|
||||
const availableCharacters: string = getAvailableCharacters(options);
|
||||
|
||||
for (let i = password.length; i < options.length; i++)
|
||||
{
|
||||
const character: string = pickRandomFromArray(availableCharacters);
|
||||
|
||||
if (options.excludeRepeating && password.includes(character))
|
||||
i--;
|
||||
else
|
||||
password += character;
|
||||
}
|
||||
|
||||
password = shuffleString(password);
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates options for password generation
|
||||
* @param options Options for password generation
|
||||
* @throws Error if options are invalid
|
||||
*/
|
||||
export function validateOptions(options: PasswordProps): void
|
||||
{
|
||||
if (options.length < 4)
|
||||
throw new Error(i18n.t("errors.too_short"));
|
||||
|
||||
const availableCharacters: string = getAvailableCharacters(options);
|
||||
|
||||
if (availableCharacters.length < 1)
|
||||
throw new Error(i18n.t("errors.no_characters"));
|
||||
|
||||
if (options.excludeRepeating && options.length > availableCharacters.length)
|
||||
throw new Error(i18n.t("errors.too_long"));
|
||||
}
|
||||
|
||||
// Returns a string containing all characters that are available for password generation
|
||||
function getAvailableCharacters(options: PasswordProps): string
|
||||
{
|
||||
let availableCharacters: string = "";
|
||||
|
||||
for (const [key, value] of Object.entries(Characters))
|
||||
if (options[key as keyof PasswordProps])
|
||||
availableCharacters += value;
|
||||
|
||||
if (options.custom && options.customSet.length > 0)
|
||||
availableCharacters += options.customSet;
|
||||
|
||||
if (options.excludeSimilar)
|
||||
availableCharacters = availableCharacters.replace(new RegExp(`[${similar}]`, "g"), "");
|
||||
|
||||
if (options.special && !options.excludeAmbiguous)
|
||||
availableCharacters += ambiguous;
|
||||
|
||||
if (options.excludeCustom.length > 0)
|
||||
availableCharacters = availableCharacters.replace(new RegExp(`[${options.excludeCustom}]`, "g"), "");
|
||||
|
||||
return availableCharacters;
|
||||
}
|
||||
|
||||
// Returns a string containing all characters from every available set that are required for password generation
|
||||
function getRequiredCharacters(options: PasswordProps): string
|
||||
{
|
||||
let result: string = "";
|
||||
const characters: Record<string, string> = Object.assign({}, Characters);
|
||||
|
||||
if (!options.excludeAmbiguous)
|
||||
characters.special += ambiguous;
|
||||
|
||||
if (options.custom && options.customSet.length > 0)
|
||||
characters.custom = options.customSet;
|
||||
|
||||
for (const key of Object.keys(characters))
|
||||
{
|
||||
if (options.excludeSimilar)
|
||||
characters[key] = characters[key].replace(new RegExp(`[${similar}]`, "g"), "");
|
||||
|
||||
if (options.excludeCustom.length > 0)
|
||||
characters[key] = characters[key].replace(new RegExp(`[${options.excludeCustom}]`, "g"), "");
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(characters))
|
||||
if (options[key as keyof PasswordProps])
|
||||
for (let i = 0; i < (options[key as keyof PasswordProps] as number); i++)
|
||||
{
|
||||
if (value.length < 1)
|
||||
continue;
|
||||
|
||||
const char = pickRandomFromArray(value);
|
||||
|
||||
if (options.excludeRepeating && result.includes(char))
|
||||
i--;
|
||||
else
|
||||
result += char;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export type PasswordProps =
|
||||
{
|
||||
length: number;
|
||||
|
||||
special: boolean | number;
|
||||
numeric: boolean | number;
|
||||
lowercase: boolean | number;
|
||||
uppercase: boolean | number;
|
||||
custom: boolean | number;
|
||||
|
||||
customSet: string;
|
||||
|
||||
excludeSimilar: boolean;
|
||||
excludeAmbiguous: boolean;
|
||||
|
||||
excludeRepeating: boolean;
|
||||
excludeCustom: string;
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
// Picks a random character from a string
|
||||
export function pickRandomFromArray(array: string): string
|
||||
{
|
||||
return array[getRandomInt(0, array.length)];
|
||||
}
|
||||
|
||||
// See https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/random
|
||||
// min is inclusive, max is exclusive
|
||||
export function getRandomInt(min: number, max: number): number
|
||||
{
|
||||
const arr = new Uint16Array(1);
|
||||
crypto.getRandomValues(arr); // Using crypto instead of Math.random() as a CSPRNG
|
||||
return Math.floor((arr[0] / 65_536) * (max - min)) + min;
|
||||
}
|
||||
|
||||
// Shuffles a string using Fisher-Yates algorithm and CSPRNG
|
||||
// See https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
|
||||
export function shuffleString(str: string): string
|
||||
{
|
||||
const arr = str.split("");
|
||||
|
||||
for (let i = arr.length - 1; i > 0; i--)
|
||||
{
|
||||
const j = getRandomInt(0, i + 1);
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
|
||||
return arr.join("");
|
||||
}
|
||||
|
||||
export function getBooleanSequence(length: number): boolean[]
|
||||
{
|
||||
const arr = new Uint8Array(Math.ceil(length / 8));
|
||||
crypto.getRandomValues(arr);
|
||||
|
||||
const result: boolean[] = [];
|
||||
|
||||
for (let i = 0; i < length; i++)
|
||||
{
|
||||
const byte = arr[Math.floor(i / 8)];
|
||||
const bit = byte & (1 << (i % 8));
|
||||
result.push(bit !== 0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { InfoLabel, Label, LabelProps, Slot } from "@fluentui/react-components";
|
||||
|
||||
// FIXME: Remove ts-ignore comments once slots override fix is released
|
||||
// Tracker: https://github.com/microsoft/fluentui/issues/27090
|
||||
|
||||
export default function infoLabel(label: string, hint: string): Slot<typeof Label>
|
||||
{
|
||||
// @ts-expect-error See FIXME
|
||||
return {
|
||||
children: (_: unknown, props: LabelProps) =>
|
||||
<InfoLabel { ...props } info={ hint }>
|
||||
{ label }
|
||||
</InfoLabel>
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Manifest } from "webextension-polyfill";
|
||||
|
||||
export const personalLinks =
|
||||
{
|
||||
website: "https://xfox111.net",
|
||||
social: "https://bsky.app/profile/xfox111.net",
|
||||
donation: "https://buymeacoffee.com/xfox111"
|
||||
};
|
||||
|
||||
export const storeLinks =
|
||||
{
|
||||
chrome: "https://chrome.google.com/webstore/detail/password-generator/jnjobgjobffgmgfnkpkjfjkkfhfikmfl",
|
||||
edge: "https://microsoftedge.microsoft.com/addons/detail/password-generator/manimdhobjbkfpeeehlhhneookiokpbj",
|
||||
firefox: "https://addons.mozilla.org/firefox/addon/easy-password-generator"
|
||||
};
|
||||
|
||||
const getGithub = (path?: string): string =>
|
||||
new URL(path ?? "", "https://github.com/xfox111/PasswordGeneratorExtension/").href;
|
||||
|
||||
export const githubLinks =
|
||||
{
|
||||
repository: getGithub(),
|
||||
changelog: getGithub("releases/latest"),
|
||||
translationGuide: getGithub("wiki/Contribution-Guidelines#contributing-to-translations"),
|
||||
license: getGithub("blob/main/LICENSE")
|
||||
};
|
||||
|
||||
export const getFeedbackLink = () =>
|
||||
{
|
||||
if (import.meta.env.FIREFOX)
|
||||
return storeLinks.firefox;
|
||||
|
||||
const manifest: Manifest.WebExtensionManifest = browser.runtime.getManifest();
|
||||
const updateUrl: URL = new URL((manifest as unknown as Record<string, unknown>).update_url as string ?? "about:blank");
|
||||
|
||||
if (updateUrl.host === "edge.microsoft.com")
|
||||
return storeLinks.edge;
|
||||
if (updateUrl.host === "clients2.google.com")
|
||||
return storeLinks.chrome;
|
||||
|
||||
return "mailto:feedback@xfox111.net";
|
||||
};
|
||||
@@ -6,9 +6,13 @@ export default class GeneratorOptions
|
||||
public Numeric: boolean = true;
|
||||
public Lowercase: boolean = true;
|
||||
public Uppercase: boolean = true;
|
||||
public Custom: boolean = false;
|
||||
|
||||
public ExcludeSimilar: boolean = true;
|
||||
public ExcludeAmbiguous: boolean = true;
|
||||
|
||||
public ExcludeRepeating: boolean = false;
|
||||
public ExcludeCustom: boolean = false;
|
||||
|
||||
public ExcludeCustomSet: string = "";
|
||||
public IncludeCustomSet: string = "";
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import browser from "webextension-polyfill";
|
||||
import ExtensionOptions from "../Models/ExtensionOptions";
|
||||
import GeneratorOptions from "../Models/GeneratorOptions";
|
||||
import ExtensionOptions from "./ExtensionOptions";
|
||||
import GeneratorOptions from "./GeneratorOptions";
|
||||
|
||||
const defaultOptions: IStorage =
|
||||
{
|
||||
@@ -14,7 +13,7 @@ const Storage = createContext<IStorage>(defaultOptions);
|
||||
|
||||
export const useStorage = () => useContext<IStorage>(Storage);
|
||||
|
||||
export function StorageProvider(props: IStorageProviderProps): JSX.Element
|
||||
export const StorageProvider: React.FC<IStorageProviderProps> = (props) =>
|
||||
{
|
||||
const [storage, setStorage] = useState<IStorage | null>(null);
|
||||
|
||||
@@ -37,7 +36,7 @@ export function StorageProvider(props: IStorageProviderProps): JSX.Element
|
||||
{ storage ? props.children : props.loader }
|
||||
</Storage.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// #region Types
|
||||
interface IStorage
|
||||
@@ -0,0 +1,5 @@
|
||||
import ExtensionOptions from "./ExtensionOptions";
|
||||
import GeneratorOptions from "./GeneratorOptions";
|
||||
|
||||
export { useStorage, StorageProvider } from "./Storage";
|
||||
export { ExtensionOptions, GeneratorOptions };
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export function useTimeout(timeout: number)
|
||||
export default function useTimeout(timeout: number)
|
||||
{
|
||||
const [isActive, setActive] = useState<boolean>(false);
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import webExtension, { readJsonFile } from "vite-plugin-web-extension";
|
||||
import path from "node:path";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
|
||||
const target: string = process.env.TARGET || "chrome";
|
||||
|
||||
function generateManifest()
|
||||
{
|
||||
const manifest = readJsonFile("src/manifest.json");
|
||||
const pkg = readJsonFile("package.json");
|
||||
return {
|
||||
version: pkg.version,
|
||||
...manifest,
|
||||
};
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
define:
|
||||
{
|
||||
__BROWSER__: JSON.stringify(target)
|
||||
},
|
||||
plugins:
|
||||
[
|
||||
react(),
|
||||
svgr(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: "src/Data/Locales",
|
||||
dest: ".",
|
||||
rename: "_locales"
|
||||
}
|
||||
]
|
||||
}),
|
||||
webExtension({
|
||||
manifest: generateManifest,
|
||||
browser: target,
|
||||
/* webExtConfig: {
|
||||
args: target === "firefox" ? [ ] : [ "--no-sandbox" ]
|
||||
} */
|
||||
}),
|
||||
],
|
||||
resolve:
|
||||
{
|
||||
alias:
|
||||
{
|
||||
// In dev mode, make sure fast refresh works
|
||||
"/@react-refresh": path.resolve(
|
||||
"node_modules/@vitejs/plugin-react-swc/refresh-runtime.js"
|
||||
),
|
||||
},
|
||||
},
|
||||
build:
|
||||
{
|
||||
chunkSizeWarningLimit: 1000,
|
||||
},
|
||||
});
|
||||