Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75b6ccd2d6 | |||
| 96c8a6a8d6 | |||
| 8126886fb5 | |||
| c0cab1c0d1 | |||
| acac03feab | |||
| 7db0aa5f24 | |||
| 016e3bfba0 | |||
| 56ae964c04 | |||
| f4674265e4 | |||
| b4117e430f | |||
| 3ecb6c4a31 | |||
| f2683e37b2 | |||
| f6ff9cf4c9 | |||
| 88b954fcd0 | |||
| 59d993ae4a | |||
| d808bffff2 | |||
| 01d048e298 | |||
| 0b47bf93aa | |||
| 53856a8b3b | |||
| 2c70ca2490 | |||
| 2731ccbff2 | |||
| cf99eb3b52 | |||
| 59d57d99a9 | |||
| 16613253ad | |||
| db8c99be0a | |||
| cfe8099ae5 | |||
| 992949f797 | |||
| e7599ea2a1 | |||
| 7e7b6f71a4 | |||
| 3e137f6955 | |||
| 47888e4774 | |||
| 1c378978c1 | |||
| 2e4e49e0db | |||
| 4a11ebbb45 | |||
| 86c4e0498b | |||
| c7869010b6 | |||
| 6f9c13587d | |||
| 5d4088dd1a | |||
| 917884b883 | |||
| 33b3df7433 | |||
| 408d417c3f | |||
| f8a80d4a4c | |||
| 32bcdef7ec |
@@ -0,0 +1,19 @@
|
||||
FROM mcr.microsoft.com/devcontainers/base:focal
|
||||
|
||||
RUN apt update && apt upgrade -y
|
||||
|
||||
RUN apt install -y software-properties-common apt-transport-https ca-certificates curl gnupg
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN apt update && apt install -y nodejs
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
RUN echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list
|
||||
RUN curl -fSsL https://dl.google.com/linux/linux_signing_key.pub | sudo gpg --dearmor | sudo tee /usr/share/keyrings/google-chrome.gpg >> /dev/null
|
||||
RUN echo deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main | sudo tee /etc/apt/sources.list.d/google-chrome.list
|
||||
|
||||
RUN apt update && apt install -y google-chrome-stable firefox
|
||||
@@ -0,0 +1,25 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||
{
|
||||
"name": "PasswordGeneratorExtension",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"postCreateCommand": "npm install"
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve the extension
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: xfox111
|
||||
---
|
||||
|
||||
### Description
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### Reproduction steps
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See error
|
||||
|
||||
### Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
### Environment
|
||||
Please provide the following information:
|
||||
- Operating System: [e.g. Windows 10 Pro 1909 (10.0.18363)]
|
||||
- Browser: [e.g. Microsoft Edge 83.0.478.56]
|
||||
- Extension version: [e.g. 1.5]
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
||||
|
||||
- [ ] Are you willing to submit a pull request to implement this feature yourself?
|
||||
@@ -0,0 +1,107 @@
|
||||
name: "🐞 Bug Report"
|
||||
description: Create a report to help us improve the extension
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "needs-triage"]
|
||||
assignees:
|
||||
- xfox111
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- type: textarea
|
||||
id: desc
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: e.g. Sometimes when generating a password not all character sets are included
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Precisely describe minimal number of steps that make the bug to appear
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See '...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
placeholder: e.g. Generated password should include at least one character from every enabled character set
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshot
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system
|
||||
options:
|
||||
- "Windows 10 and newer"
|
||||
- "Windows 8/8.1"
|
||||
- "Windows 7 and older"
|
||||
- "MacOS"
|
||||
- "Debian or Debian-based"
|
||||
- "Other"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser name and version
|
||||
placeholder: e.g. Microsoft Edge 119.0.2151.58
|
||||
description: Put here your browser's name and version
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Extension version
|
||||
placeholder: e.g. 3.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: requested-help
|
||||
attributes:
|
||||
label: Are you willing to submit a PR for this issue?
|
||||
options:
|
||||
- "yes"
|
||||
- "no"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
|
||||
required: true
|
||||
- label: The provided reproduction is a minimal reproducible example of the bug.
|
||||
required: true
|
||||
@@ -0,0 +1,7 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/XFox111/PasswordGeneratorExtension/discussions
|
||||
about: Use GitHub discussions for message-board style questions and discussions.
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: xfox111
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when '...'
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
- [ ] Are you willing to submit a pull request to implement this feature yourself?
|
||||
@@ -0,0 +1,62 @@
|
||||
name: "🚀 New feature proposal"
|
||||
description: Suggest a feature idea for this project
|
||||
title: "[Feature]: "
|
||||
labels: ["feature", "needs-triage"]
|
||||
assignees:
|
||||
- xfox111
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for your interest in the project and taking the time to fill out this feature report!
|
||||
|
||||
- type: textarea
|
||||
id: proposition
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: Describe the solution you'd like
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: justification
|
||||
attributes:
|
||||
label: Justification
|
||||
description: Is your feature request related to a problem? Please describe.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alts
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: Describe alternatives you've considered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: requested-help
|
||||
attributes:
|
||||
label: Are you willing to submit a PR for this issue?
|
||||
options:
|
||||
- "yes"
|
||||
- "no"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
|
||||
required: true
|
||||
@@ -10,20 +10,36 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
target-branch: "main"
|
||||
target-branch: "next"
|
||||
assignees:
|
||||
- "xfox111"
|
||||
reviewers:
|
||||
- "xfox111"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
target-branch: "main"
|
||||
target-branch: "next"
|
||||
assignees:
|
||||
- "xfox111"
|
||||
reviewers:
|
||||
- "xfox111"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
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,14 +1,33 @@
|
||||
## Description
|
||||
> Put short description of the pull request here
|
||||
<!-- ⚠️ Make sure that you create this PR against `next` branch and not `main` -->
|
||||
|
||||
fixes: #issue_number
|
||||
## Description
|
||||
<!--Put short description of the pull request here-->
|
||||
|
||||
Resolves: #issue_number
|
||||
|
||||
<!-- ------------------------------------- -->
|
||||
<!-- FOR REPOSITORY MAINTAINERS' PRS ONLY! -->
|
||||
<!-- DO NOT INCLUDE FOLLOWING IN YOUR PR!! -->
|
||||
<!-- ------------------------------------- -->
|
||||
|
||||
<!-- > ## 🚀 Patch Tuesday update
|
||||
> This pull request is a part of our new initiative!
|
||||
From now on we are starting to roll out updates on every first Tuesday of the month, which will include bugfixes, security and dependency updates to keep the project's security and stability up to date!
|
||||
|
||||
## Description
|
||||
Dependencies update and security fixes
|
||||
|
||||
## Changelog
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
### Dependency bumps
|
||||
- #
|
||||
### Fixed security vulnerabilities
|
||||
- [CWE-20](https://cwe.mitre.org/data/definitions/20.html) (#)
|
||||
- CVE-2022-25883 (#)
|
||||
|
||||
## PR Checklist (main branch)
|
||||
- [ ] Update extension version in `package.json`
|
||||
- [ ] [Post-merge] Create a release to publish the new version
|
||||
## PR Checklist
|
||||
- [ ] Update version in `package.json`
|
||||
- [ ] [Post-merge] Review and publish GitHub release
|
||||
- [ ] Update Discussions
|
||||
- [ ] [Post-deploy] Update changelog for Firefox webstore
|
||||
- [ ] Reset `next` branch to be in sync with `main`
|
||||
-->
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
<!-- > ## 🚀 Patch Tuesday update
|
||||
> This release is a part of our new initiative!
|
||||
From now on we are starting to roll out updates on every first Tuesday of the month, which will include bugfixes, security and dependency updates to keep the project's security and stability up to date!
|
||||
-->
|
||||
|
||||
## What's new
|
||||
-
|
||||
<!-- - Dependency updates and security patches (#) -->
|
||||
|
||||
## Installation
|
||||
### From extension webstore (recommended)
|
||||
- [Google Chrome Webstore](https://chrome.google.com/webstore/detail/jnjobgjobffgmgfnkpkjfjkkfhfikmfl)
|
||||
- [Microsoft Edge Add-ons Webstore](https://microsoftedge.microsoft.com/addons/detail/manimdhobjbkfpeeehlhhneookiokpbj)
|
||||
- [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/easy-password-generator/)
|
||||
- [GitHub Releases](https://github.com/xfox111/PasswordGeneratorExtension/releases/latest)
|
||||
<!-- ### Fixed security issues in this update
|
||||
- [CWE-20](https://cwe.mitre.org/data/definitions/20.html)
|
||||
- CVE-2022-25883
|
||||
-->
|
||||
|
||||
Note that version published on these webstores can differ from this release
|
||||
### Sideloading (for testing purposes only)
|
||||
1. Download attached archive and unpack it
|
||||
2. Enable Developers mode on your browser extensions page
|
||||
3. Click "Load unpacked" button and navigate to the extension root folder (contains `manifest.json`)
|
||||
4. Done!
|
||||
|
||||
*On Firefox you should open manifest file instead of extension's folder
|
||||
|
||||
**Note:** If you delete extension folder it will disappear from your browser
|
||||
|
||||
_Sideloaded extensions don't replace officially installed ones_
|
||||
Refer to [Download section of the README.md](https://github.com/XFox111/PasswordGeneratorExtension#download) for sideloading instructions and download links
|
||||
|
||||
@@ -1,169 +1,144 @@
|
||||
name: Release pipeline
|
||||
|
||||
on:
|
||||
|
||||
release:
|
||||
types: [published]
|
||||
types: [ released ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bypass_audit:
|
||||
description: Bypass npm audit
|
||||
type: boolean
|
||||
default: false
|
||||
targets:
|
||||
description: Targets
|
||||
required: true
|
||||
default: '["chrome","firefox"]'
|
||||
type: choice
|
||||
options:
|
||||
- '["chrome","firefox"]'
|
||||
- '["chrome"]'
|
||||
- '["firefox"]'
|
||||
firefox:
|
||||
description: Deploy Firefox
|
||||
type: boolean
|
||||
default: true
|
||||
chrome:
|
||||
description: Deploy Chrome
|
||||
type: boolean
|
||||
default: true
|
||||
edge:
|
||||
description: Deploy Edge
|
||||
type: boolean
|
||||
default: true
|
||||
gh-release:
|
||||
description: Attach to GitHub release
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
Build:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: ${{ fromJSON(github.event.inputs.targets || '["chrome","firefox"]') }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
|
||||
- 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: ./.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: ./.output/firefox-mv3
|
||||
self-hosted: false
|
||||
|
||||
- run: npm audit
|
||||
if: ${{ github.event_name == 'release' || github.event.inputs.bypass_audit == 'false' }}
|
||||
|
||||
publish-github:
|
||||
needs: build
|
||||
if: ${{ github.event_name == 'release' || github.event.inputs.gh-release == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: ${{ fromJSON(github.event.inputs.targets || '["chrome","firefox"]') }}
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@main
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
|
||||
- name: Attach build to release
|
||||
uses: xresloader/upload-to-github-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
file: password-generator-*-${{ matrix.target }}.zip
|
||||
draft: false
|
||||
overwrite: true
|
||||
update_latest_release: true
|
||||
|
||||
publish-chrome:
|
||||
needs: build
|
||||
if: ${{ github.event_name == 'release' || (github.event.inputs.chrome == 'true' && contains(github.event.inputs.targets, 'chrome')) }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@main
|
||||
with:
|
||||
name: chrome
|
||||
|
||||
- name: Configure manifest
|
||||
uses: Amadevus/pwsh-script@v2
|
||||
with:
|
||||
script: |
|
||||
[PSCustomObject] $package = Get-Content "package.json" | ConvertFrom-Json;
|
||||
- uses: wdzeng/chrome-extension@v1.3.0
|
||||
with:
|
||||
extension-id: jnjobgjobffgmgfnkpkjfjkkfhfikmfl
|
||||
zip-path: password-generator-*-chrome.zip
|
||||
client-id: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
|
||||
[PSCustomObject] $manifest = Get-Content "public/manifest.json" | ConvertFrom-Json;
|
||||
$manifest.version = $package.version;
|
||||
$manifest | ConvertTo-Json -Depth 10 | Out-File "public/manifest.json"
|
||||
|
||||
$manifest = Get-Content "public/manifest.v2.json" | ConvertFrom-Json;
|
||||
$manifest.version = $package.version;
|
||||
$manifest | ConvertTo-Json -Depth 10 | Out-File "public/manifest.v2.json"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: yarn
|
||||
|
||||
- run: yarn install
|
||||
- run: yarn build
|
||||
|
||||
- name: Drop artifacts (build)
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
|
||||
Pack-Chromium:
|
||||
needs: Build
|
||||
publish-edge:
|
||||
needs: build
|
||||
if: ${{ github.event_name == 'release' || (github.event.inputs.edge == 'true' && contains(github.event.inputs.targets, 'chrome')) }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
- uses: actions/download-artifact@main
|
||||
with:
|
||||
name: chrome
|
||||
|
||||
- name: Configure manifest
|
||||
uses: Amadevus/pwsh-script@v2
|
||||
with:
|
||||
script: Remove-Item "manifest.v2.json"
|
||||
- uses: wdzeng/edge-addon@v2.1.0
|
||||
with:
|
||||
product-id: ${{ secrets.EDGE_PRODUCT_ID }}
|
||||
zip-path: password-generator-*-chrome.zip
|
||||
client-id: ${{ secrets.EDGE_CLIENT_ID }}
|
||||
api-key: ${{ secrets.EDGE_API_KEY }}
|
||||
|
||||
- name: Pack extension
|
||||
uses: TheDoctor0/zip-release@0.6.2
|
||||
with:
|
||||
filename: PasswordGenerator-Chromium.zip
|
||||
|
||||
- name: Drop artifacts (Chromium)
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: Chromium
|
||||
path: PasswordGenerator-Chromium.zip
|
||||
|
||||
- name: Attach build to release
|
||||
uses: xresloader/upload-to-github-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
file: PasswordGenerator-Chromium.zip
|
||||
draft: false
|
||||
update_latest_release: true
|
||||
|
||||
Pack-Firefox:
|
||||
needs: Build
|
||||
publish-firefox:
|
||||
needs: build
|
||||
if: ${{ github.event_name == 'release' || (github.event.inputs.firefox == 'true' && contains(github.event.inputs.targets, 'firefox')) }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
- uses: actions/download-artifact@main
|
||||
with:
|
||||
name: firefox
|
||||
|
||||
- name: Configure manifest
|
||||
uses: Amadevus/pwsh-script@v2
|
||||
with:
|
||||
script: |
|
||||
Remove-Item "manifest.json"
|
||||
Rename-Item "manifest.v2.json" "manifest.json"
|
||||
|
||||
- name: Pack extension
|
||||
uses: TheDoctor0/zip-release@0.6.2
|
||||
with:
|
||||
filename: PasswordGenerator-Firefox.zip
|
||||
|
||||
- name: Drop artifacts (Firefox)
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: Firefox
|
||||
path: PasswordGenerator-Firefox.zip
|
||||
|
||||
- name: Attach build to release
|
||||
uses: xresloader/upload-to-github-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
file: PasswordGenerator-Firefox.zip
|
||||
draft: false
|
||||
update_latest_release: true
|
||||
|
||||
Chrome:
|
||||
needs: Pack-Chromium
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download artifacts (Chromium)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Chromium
|
||||
|
||||
- name: Publish to Chrome Web Store
|
||||
uses: wdzeng/chrome-extension@v1
|
||||
with:
|
||||
extension-id: jnjobgjobffgmgfnkpkjfjkkfhfikmfl
|
||||
zip-path: PasswordGenerator-Chromium.zip
|
||||
client-id: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
|
||||
Edge:
|
||||
needs: Pack-Chromium
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download artifacts (Chromium)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Chromium
|
||||
|
||||
- name: Publish to Edge Addons
|
||||
uses: wdzeng/edge-addon@v1
|
||||
with:
|
||||
product-id: ${{ secrets.EDGE_PRODUCT_ID }}
|
||||
zip-path: PasswordGenerator-Chromium.zip
|
||||
client-id: ${{ secrets.EDGE_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.EDGE_CLIENT_SECRET }}
|
||||
access-token-url: ${{ secrets.EDGE_ACCESS_TOKEN_URL }}
|
||||
|
||||
Firefox:
|
||||
needs: Pack-Firefox
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download artifacts (Firefox)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Firefox
|
||||
|
||||
- name: Publish to Firefox
|
||||
uses: wdzeng/firefox-addon@v1
|
||||
with:
|
||||
addon-guid: ${{ secrets.FIREFOX_EXT_UUID }}
|
||||
xpi-path: PasswordGenerator-Firefox.zip
|
||||
jwt-issuer: ${{ secrets.FIREFOX_API_KEY }}
|
||||
jwt-secret: ${{ secrets.FIREFOX_CLIENT_SECRET }}
|
||||
- uses: wdzeng/firefox-addon@v1.1.2
|
||||
with:
|
||||
addon-guid: ${{ secrets.FIREFOX_EXT_UUID }}
|
||||
xpi-path: password-generator-*-firefox.zip
|
||||
jwt-issuer: ${{ secrets.FIREFOX_API_KEY }}
|
||||
jwt-secret: ${{ secrets.FIREFOX_CLIENT_SECRET }}
|
||||
|
||||
@@ -13,24 +13,24 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: [ "main", "next" ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- 'PRIVACY'
|
||||
- '**/cd_pipeline.yaml'
|
||||
- '**/dependabot.yml'
|
||||
- '**/pr_pipeline.yaml'
|
||||
- '.vscode/*'
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
branches: [ "main", "next" ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- 'PRIVACY'
|
||||
- '**/cd_pipeline.yaml'
|
||||
- '**/dependabot.yml'
|
||||
- '**/pr_pipeline.yaml'
|
||||
- '.vscode/*'
|
||||
schedule:
|
||||
- cron: '24 7 * * 3'
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript', 'typescript' ]
|
||||
language: [ 'typescript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -83,4 +83,4 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: PR next workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'package.json'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-release-draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
|
||||
- name: Get version from package.json
|
||||
id: get_version
|
||||
run: |
|
||||
extver=`jq -r ".version" package.json`
|
||||
echo "version=$extver" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: dev-build-deploy/release-me@v0.18.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: v
|
||||
draft: true
|
||||
version: v${{ steps.get_version.outputs.version }}
|
||||
release-notes: .github/release_description_template.md
|
||||
@@ -2,8 +2,7 @@ name: PR check pipeline
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
branches: [ "main", "next" ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
@@ -11,72 +10,48 @@ on:
|
||||
- '**/cd_pipeline.yaml'
|
||||
- '**/dependabot.yml'
|
||||
- '**/codeql-analysis.yml'
|
||||
- '**/pr_next.yaml'
|
||||
- '.vscode/*'
|
||||
- '.devcontainer/*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
targets:
|
||||
description: Targets
|
||||
required: true
|
||||
default: '["chrome","firefox"]'
|
||||
type: choice
|
||||
options:
|
||||
- '["chrome","firefox"]'
|
||||
- '["chrome"]'
|
||||
- '["firefox"]'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: ${{ fromJSON(github.event.inputs.targets || '["chrome","firefox"]') }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@main
|
||||
|
||||
- name: Configure manifest
|
||||
uses: Amadevus/pwsh-script@v2
|
||||
with:
|
||||
script: |
|
||||
[PSCustomObject] $package = Get-Content "package.json" | ConvertFrom-Json;
|
||||
[PSCustomObject] $manifest = Get-Content "public/manifest.json" | ConvertFrom-Json;
|
||||
$manifest.version = $package.version;
|
||||
$manifest | ConvertTo-Json -Depth 10 | Out-File "public/manifest.json"
|
||||
$manifest = Get-Content "public/manifest.v2.json" | ConvertFrom-Json;
|
||||
$manifest.version = $package.version;
|
||||
$manifest | ConvertTo-Json -Depth 10 | Out-File "public/manifest.v2.json"
|
||||
- run: npm install
|
||||
- run: npm run zip -- -b ${{ matrix.target }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
cache: yarn
|
||||
- name: Drop artifacts (${{ matrix.target }})
|
||||
uses: actions/upload-artifact@main
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: ./.output/password-generator-*-${{ matrix.target }}.zip
|
||||
include-hidden-files: true
|
||||
|
||||
- run: yarn install
|
||||
- run: yarn build
|
||||
- name: web-ext lint
|
||||
if: ${{ matrix.target == 'firefox' }}
|
||||
uses: freaktechnik/web-ext-lint@main
|
||||
with:
|
||||
extension-root: ./.output/firefox-mv3
|
||||
self-hosted: false
|
||||
|
||||
- name: Drop artifacts (build)
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
|
||||
firefox-check:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
|
||||
- name: Configure manifest
|
||||
uses: Amadevus/pwsh-script@v2
|
||||
with:
|
||||
script: |
|
||||
Remove-Item "manifest.json"
|
||||
Rename-Item "manifest.v2.json" "manifest.json"
|
||||
|
||||
- name: "web-ext lint"
|
||||
uses: kewisch/action-web-ext@e0ea88d527a8a90bc10d600f80ac667d219e6bf1
|
||||
with:
|
||||
cmd: lint
|
||||
source: .
|
||||
channel: listed
|
||||
|
||||
- name: Pack extension
|
||||
uses: TheDoctor0/zip-release@0.6.2
|
||||
with:
|
||||
filename: PasswordGenerator-Firefox.zip
|
||||
|
||||
- name: Drop artifacts (Firefox)
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: Firefox
|
||||
path: PasswordGenerator-Firefox.zip
|
||||
- run: npm audit
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# IDE files
|
||||
.vscode
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"bierner.github-markdown-preview",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"github.vscode-github-actions",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"jock.svg",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"saeris.markdown-github-alerts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"editor.rulers": [
|
||||
{
|
||||
"column": 120
|
||||
}
|
||||
],
|
||||
"editor.insertSpaces": false,
|
||||
"files.insertFinalNewline": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"files.trimFinalNewlines": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"css.format.braceStyle": "expand",
|
||||
"css.format.maxPreserveNewLines": 3,
|
||||
"css.format.spaceAroundSelectorSeparator": true,
|
||||
"css.format.newlineBetweenSelectors": false,
|
||||
"css.lint.compatibleVendorPrefixes": "warning",
|
||||
"css.lint.duplicateProperties": "warning",
|
||||
"css.lint.float": "warning",
|
||||
"css.lint.unknownVendorSpecificProperties": "warning",
|
||||
"css.lint.zeroUnits": "warning",
|
||||
"html.format.maxPreserveNewLines": 3,
|
||||
"html.format.wrapAttributes": "preserve",
|
||||
"javascript.format.placeOpenBraceOnNewLineForControlBlocks": true,
|
||||
"javascript.format.placeOpenBraceOnNewLineForFunctions": true,
|
||||
"javascript.format.semicolons": "insert",
|
||||
"javascript.preferences.quoteStyle": "double",
|
||||
"javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": true,
|
||||
"typescript.format.placeOpenBraceOnNewLineForControlBlocks": true,
|
||||
"typescript.format.placeOpenBraceOnNewLineForFunctions": true,
|
||||
"typescript.format.semicolons": "insert",
|
||||
"typescript.preferences.quoteStyle": "double",
|
||||
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": true
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build -- -b chrome",
|
||||
"group":
|
||||
{
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"label": "Build: Chromium"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build -- -b firefox",
|
||||
"group": "build",
|
||||
"label": "Build: Firefox"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev -- -b chrome",
|
||||
"group": "test",
|
||||
"label": "Dev: Google Chrome"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev -- -b firefox",
|
||||
"group": "test",
|
||||
"label": "Dev: Firefox"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev -- -b edge",
|
||||
"group": "test",
|
||||
"label": "Dev: Microsoft Edge"
|
||||
},
|
||||
{
|
||||
"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,293 +1,4 @@
|
||||
# Contribution Guidelines
|
||||
Welcome, and thank you for your interest in contributing to my project!
|
||||
|
||||
There are many ways in which you can contribute, beyond writing code. The goal of this document is to provide a high-level overview of how you can get involved.
|
||||
|
||||
## Table of Contents
|
||||
- [Contribution Guidelines](#contribution-guidelines)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Asking Questions](#asking-questions)
|
||||
- [Providing Feedback](#providing-feedback)
|
||||
- [Reporting Issues](#reporting-issues)
|
||||
- [Look For an Existing Issue](#look-for-an-existing-issue)
|
||||
- [Writing Good Bug Reports and Feature Requests](#writing-good-bug-reports-and-feature-requests)
|
||||
- [Final Checklist](#final-checklist)
|
||||
- [Follow Your Issue](#follow-your-issue)
|
||||
- [Contributing to codebase](#contributing-to-codebase)
|
||||
- [Deploy test version on your browser](#deploy-test-version-on-your-browser)
|
||||
- [Development workflow](#development-workflow)
|
||||
- [Release](#release)
|
||||
- [Coding guidelines](#coding-guidelines)
|
||||
- [Indentation](#indentation)
|
||||
- [Names](#names)
|
||||
- [Comments](#comments)
|
||||
- [Strings](#strings)
|
||||
- [Style](#style)
|
||||
- [Finding an issue to work on](#finding-an-issue-to-work-on)
|
||||
- [Contributing to translations](#contributing-to-translations)
|
||||
- [Submitting pull requests](#submitting-pull-requests)
|
||||
- [Spell check errors](#spell-check-errors)
|
||||
- [Thank You!](#thank-you)
|
||||
- [Attribution](#attribution)
|
||||
|
||||
## Asking Questions
|
||||
Have a question? Rather than opening an issue, please ask me directly on opensource@xfox111.net.
|
||||
|
||||
## Providing Feedback
|
||||
Your comments and feedback are welcome.
|
||||
You can leave your feedbak on feedback@xfox111.net or do it on [Microsoft Edge Add-ons Webstore](https://microsoftedge.microsoft.com/addons/detail/manimdhobjbkfpeeehlhhneookiokpbj), [Chrome Extensions Webstore](https://chrome.google.com/webstore/detail/jnjobgjobffgmgfnkpkjfjkkfhfikmfl) or [Mozilla Add-ons Webstore](https://addons.mozilla.org/en-US/firefox/addon/easy-password-generator/)
|
||||
|
||||
## Reporting Issues
|
||||
Have you identified a reproducible problem in the extension? Have a feature request? I'd like to hear it! Here's how you can make reporting your issue as effective as possible.
|
||||
|
||||
### Look For an Existing Issue
|
||||
Before you create a new issue, please do a search in [open issues](https://github.com/xfox111/PasswordGeneratorExtension/issues) to see if the issue or feature request has already been filed.
|
||||
|
||||
Be sure to scan through the [feature requests](https://github.com/XFox111/PasswordGeneratorExtension/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement).
|
||||
|
||||
If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment:
|
||||
|
||||
* 👍 - upvote
|
||||
* 👎 - downvote
|
||||
|
||||
If you cannot find an existing issue that describes your bug or feature, create a new issue using the guidelines below.
|
||||
|
||||
### Writing Good Bug Reports and Feature Requests
|
||||
File a single issue per problem and feature request. Do not enumerate multiple bugs or feature requests in the same issue.
|
||||
|
||||
Do not add your issue as a comment to an existing issue unless they are the same ones. Many issues look similar, but have different causes.
|
||||
|
||||
The more information you can provide, the more likely someone will be successful at reproducing the issue and finding a solution.
|
||||
|
||||
Please include the following with each issue:
|
||||
- Current version of the extension
|
||||
- Your current browser and OS name
|
||||
- Reproducible steps (1... 2... 3...) that cause the issue
|
||||
- What you expected to see, versus what you actually saw
|
||||
- Images, animations, or a link to a video showing the issue occurring
|
||||
|
||||
### Final Checklist
|
||||
Please remember to do the following:
|
||||
- [*] Search the issue repository to ensure your report is a new issue
|
||||
- [*] Separate issues reports
|
||||
- [*] Include as much information as you can to your report
|
||||
|
||||
Don't feel bad if the developers can't reproduce the issue right away. They will simply ask for more information!
|
||||
|
||||
### Follow Your Issue
|
||||
Once your report is submitted, be sure to stay in touch with the devs in case they need more help from you.
|
||||
|
||||
## Contributing to codebase
|
||||
If you are interested in writing code to fix issues or implement new awesome features you can follow this guidelines to get a better result
|
||||
|
||||
### Deploy test version on your browser
|
||||
1. Clone repository to local storage using [Git](https://guides.github.com/introduction/git-handbook/)
|
||||
```
|
||||
git clone https://github.com/xfox111/PasswordGeneratorExtension.git
|
||||
```
|
||||
2. Install [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/)
|
||||
3. Open terminal in project directory and run
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
4. Build project
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
2. Enable Developers mode on your browser extensions page
|
||||
3. Click "Load unpacked" button and navigate to the `build` folder of the repo (contains `manifest.json`)
|
||||
4. Done!
|
||||
|
||||
To run extension as standalone web application you can use `yarn start` command
|
||||
|
||||
Next time you make any changes to the codebase, rebuild extension with `yarn build` and reload extension by toggling it off and on or by pressing "Reload" button on extensions list page
|
||||
|
||||
### Development workflow
|
||||
This section represents how contributors should interact with codebase implementing features and fixing bugs
|
||||
1. Getting assigned to the issue
|
||||
2. Creating a repository fork
|
||||
3. Making changes to codebase
|
||||
5. Creating a pull request to `main`
|
||||
6. Reviewing & completing PR
|
||||
7. Done
|
||||
|
||||
#### Release
|
||||
Next stage is release. Release performs on every push to main (which makes functional changes to the source code). Release performs manually by @XFox111 into: Chrome, Firefox, Edge webstores as well as GitHub releases
|
||||
|
||||
### Coding guidelines
|
||||
#### Indentation
|
||||
We use tabs, not spaces.
|
||||
|
||||
#### Names
|
||||
The project naming rules inherit [.NET Naming Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/naming-guidelines). Nevertheless there'is some distinction with the guidelines as well as additions to the one:
|
||||
- Use `camelCase` for variables instead of `CamelCase` stated in [Capitalization Conventions](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/capitalization-conventions#capitalization-rules-for-identifiers)
|
||||
- Use `camelCase` for files in `public` directory
|
||||
- Use `PascalCase` for files in `src` directory
|
||||
|
||||
#### Comments
|
||||
Leave as more comments as you can. Remember: the more detailed documentation your code has the less programmers will curse you in the future
|
||||
|
||||
#### Strings
|
||||
Use "double quotes" wherever it's possible
|
||||
|
||||
#### Style
|
||||
- Prefer to use lambda functions
|
||||
- Always put curly braces on new lines
|
||||
- Wrong:
|
||||
```js
|
||||
if (condition) {
|
||||
...
|
||||
}
|
||||
```
|
||||
- Correct:
|
||||
```js
|
||||
if (condition)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
> **Note:** For JSON files put opening brace on the same line as the key
|
||||
- Put spaces between operators, conditionals and loops
|
||||
- Wrong:
|
||||
```js
|
||||
y=k*x+b;
|
||||
if(condition) { ... }
|
||||
```
|
||||
- Correct:
|
||||
```js
|
||||
y = k * x + b;
|
||||
if (condition) { ... }
|
||||
```
|
||||
- Use ternary conditionals wherever it's possible, unless it's too long
|
||||
- Wrong:
|
||||
```js
|
||||
var s;
|
||||
if (condition)
|
||||
s = "Life";
|
||||
else
|
||||
s = "Death";
|
||||
```
|
||||
- Correct:
|
||||
```js
|
||||
var s = condition ? "Life" : "Death";
|
||||
```
|
||||
- Do not surround loop and conditional bodies with curly braces if they can be avoided
|
||||
- Wrong:
|
||||
```js
|
||||
if (condition)
|
||||
{
|
||||
console.log("Hello, World!");
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
```
|
||||
- Correct
|
||||
```js
|
||||
if (condition)
|
||||
console.log("Hello, World!");
|
||||
else
|
||||
return;
|
||||
```
|
||||
- Prefer export modules as default
|
||||
- Wrong:
|
||||
```js
|
||||
export class MyClass { ... }
|
||||
```
|
||||
- Correct:
|
||||
```js
|
||||
export default class MyClass { ... }
|
||||
```
|
||||
- Prefer export modules as classes unless it is excessive
|
||||
- Wrong:
|
||||
```ts
|
||||
export function MyFunction1() { ... }
|
||||
export function MyFunction2() { ... }
|
||||
export default class MyClass2()
|
||||
{
|
||||
public static GetDate(timestamp: number): Date
|
||||
{
|
||||
return new Date(timestamp);
|
||||
}
|
||||
}
|
||||
```
|
||||
- Correct:
|
||||
```js
|
||||
export default class MyClass1
|
||||
{
|
||||
public static MyFunction1() { ... }
|
||||
public static MyFunction2() { ... }
|
||||
}
|
||||
export default GetDate(timestamp: number): Date
|
||||
{
|
||||
return new Date(timestamp);
|
||||
}
|
||||
```
|
||||
- When JSX attributes take too much space, put each attribute on a new line and put additional line before component's content
|
||||
- Wrong:
|
||||
```tsx
|
||||
<HelloWorld attribute1="value" attribute2={ value } attribute3="value">My content here</HelloWorld>
|
||||
<HelloWorld attribute1="value"
|
||||
attribute2={ value }
|
||||
attribute3="value">My content here</HelloWorld>
|
||||
<HelloWorld attribute1="value"
|
||||
attribute2={ value }
|
||||
attribute3="value">
|
||||
My content here
|
||||
</HelloWorld>
|
||||
<HelloWorld
|
||||
attribute1="value"
|
||||
attribute2={ value }
|
||||
attribute3="value">
|
||||
My content here
|
||||
</HelloWorld>
|
||||
```
|
||||
- Correct:
|
||||
```tsx
|
||||
<HelloWorld
|
||||
attribute1="value"
|
||||
attribute2={ value }
|
||||
attribute3="value">
|
||||
|
||||
My content here
|
||||
</HelloWorld>
|
||||
```
|
||||
- If JSX component doesn't have content, put space before closing tag
|
||||
- Wrong:
|
||||
```tsx
|
||||
<HelloWorld attribute1="value" attribute2={ value } attribute3="value"/>
|
||||
```
|
||||
- Correct:
|
||||
```tsx
|
||||
<HelloWorld attribute1="value" attribute2={ value } attribute3="value" />
|
||||
```
|
||||
|
||||
### Finding an issue to work on
|
||||
Check out the [full issues list](https://github.com/XFox111/PasswordGeneratorExtension/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue) for a list of all potential areas for contributions. **Note** that just because an issue exists in the repository does not mean we will accept a contribution. There are several reasons we may not accept a pull request like:
|
||||
|
||||
- Performance - One of project's core values is to deliver a lightweight extension, that means it should perform well in both real and test environments.
|
||||
- User experience - Since we want to deliver a lightweight extension, the UX should feel lightweight as well and not be cluttered. Most changes to the UI should go through the issue owner and project owner (@XFox111).
|
||||
- Architectural - Project owner needs to agree with any architectural impact a change may make. Such things must be discussed with and agreed upon by the project owner.
|
||||
|
||||
To improve the chances to get a pull request merged you should select an issue that is labelled with the `help-wanted` or `bug` labels. If the issue you want to work on is not labelled with `help-wanted` or `bug`, you can start a conversation with the project owner asking whether an external contribution will be considered.
|
||||
|
||||
To avoid multiple pull requests resolving the same issue, let others know you are working on it by saying so in a comment.
|
||||
|
||||
### Contributing to translations
|
||||
If you want to help us to translate this extension into other languages, please read [this article](https://developer.chrome.com/extensions/i18n)
|
||||
|
||||
**Note** that whatever you want to contribute to the codebase, you should do it only after you got assigned on an issue
|
||||
|
||||
### Submitting pull requests
|
||||
To enable us to quickly review and accept your pull requests, always create one pull request per issue and [link the issue in the pull request](https://github.com/blog/957-introducing-issue-mentions). Never merge multiple requests in one unless they have the same root cause. Be sure to follow our [Coding Guidelines](#coding-guidelines) and keep code changes as small as possible. Avoid pure formatting changes to code that has not been modified otherwise. Pull requests should contain tests whenever possible. Fill pull request content according to its template. Deviations from template are not recommended
|
||||
|
||||
#### Spell check errors
|
||||
Pull requests that fix typos are welcomed but please make sure it doesn't touch multiple feature areas, otherwise it will be difficult to review. Pull requests only fixing spell check errors in source code are not recommended.
|
||||
|
||||
## Thank You!
|
||||
|
||||
Your contributions to open source, large or small, make great projects like this possible. Thank you for taking the time to contribute.
|
||||
|
||||
## Attribution
|
||||
This Contribution Guidelines are adapted from the [Contributing to VS Code](https://github.com/microsoft/vscode/blob/master/CONTRIBUTING.md)
|
||||
> [!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) 2022 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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Password Generator Extension Privacy Policy
|
||||
1. Developers of the extension don't affiliate with Google LLC, Mozilla Foundation or Microsoft Corporation in any way
|
||||
2. This extension doesn't store any personal data (data which may be used to track your location or reveal your identity)
|
||||
3. This extension uses build-in cloud storage of your browser to store extension settings
|
||||
# Password generator extension Privacy policy
|
||||
1. Developers of the extension don't affiliate with Google LLC, Mozilla Foundation or Microsoft Corporation in any way.
|
||||
2. This extension doesn't store any personal data (data which may be used to track your location or reveal your identity).
|
||||
3. This extension uses cloud storage built into your browser to store its settings.
|
||||
4. Refer to your browser's developer regarding the privacy policy of the cloud storage used by your browser.
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
# Password generator
|
||||
<!-- # Password generator -->
|
||||
|
||||
[](https://github.com/xfox111/PasswordGeneratorExtension/releases/latest)
|
||||
[](https://github.com/xfox111/PasswordGeneratorExtension/commits/master)
|
||||
[](https://github.com/XFox111/PasswordGeneratorExtension/commits/main)
|
||||
|
||||
[](https://twitter.com/xfox111)
|
||||
[](https://github.com/xfox111)
|
||||
[](https://buymeacoffee.com/xfox111)
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.xfox111.net/projects/pwdgen/PasswordGeneratorExtension-dark.webp">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://cdn.xfox111.net/projects/pwdgen/PasswordGeneratorExtension-light.webp">
|
||||
<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
|
||||
- Portuguese (Brazil)
|
||||
- Russian
|
||||
|
||||
<img width="1136" alt="Password generator v2.0" src="https://user-images.githubusercontent.com/28831743/188680034-a673b1b4-3153-4054-bb0d-949568de1748.png">
|
||||
- Ukrainian
|
||||
|
||||
## Download
|
||||
[](https://chrome.google.com/webstore/detail/jnjobgjobffgmgfnkpkjfjkkfhfikmfl)
|
||||
@@ -33,6 +35,45 @@ Extension for web browsers which helps you to easily generate strong passwords i
|
||||
- [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/easy-password-generator/)
|
||||
- [GitHub Releases](https://github.com/xfox111/PasswordGeneratorExtension/releases/latest)
|
||||
|
||||
### Sideloading (for testing purposes only)
|
||||
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary><b>Chromium-based browsers (Edge, Chrome, etc.)</b></summary>
|
||||
|
||||
> 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`
|
||||
> 4. Enable "Developer mode"
|
||||
> 5. Click the "Load unpacked" button and navigate to the extension's root folder (contains `manifest.json`)
|
||||
> 6. Done!
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Firefox</b></summary>
|
||||
|
||||
> 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.json` file in the root folder
|
||||
> 5. Done!
|
||||
|
||||
> **Important!**
|
||||
This will _replace_ officialy installed version if you have one.
|
||||
If you want to sideload it without replacing to run both versions at the same time - before loading add-on, open `manifest.json` in a text editor and change `id` key (it's `passwordgenerator@xfox111.net` by default) to something else
|
||||
|
||||
</details>
|
||||
|
||||
> **Note:** If you delete the extension folder it will disappear from your browser
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
## Contributing
|
||||
[](https://github.com/xfox111/PasswordGeneratorExtension/issues)
|
||||
[](https://github.com/XFox111/PasswordGeneratorExtension/actions/workflows/cd_pipeline.yaml)
|
||||
@@ -43,18 +84,12 @@ There are many ways in which you can participate in the project, for example:
|
||||
- Review [source code changes](https://github.com/xfox111/PasswordGeneratorExtension/pulls)
|
||||
- Review documentation and make pull requests for anything from typos to new content
|
||||
|
||||
If you are interested in fixing issues and contributing directly to the code base, please see the [Contribution Guidelines](https://github.com/XFox111/PasswordGeneratorExtension/blob/master/CONTRIBUTING.md), which covers the following:
|
||||
- [How to deploy the extension on your browser](https://github.com/XFox111/PasswordGeneratorExtension/blob/master/CONTRIBUTING.md#deploy-test-version-on-your-browser)
|
||||
- [The development workflow](https://github.com/XFox111/PasswordGeneratorExtension/blob/master/CONTRIBUTING.md#development-workflow), including debugging and running tests
|
||||
- [Coding guidelines](https://github.com/XFox111/PasswordGeneratorExtension/blob/master/CONTRIBUTING.md#coding-guidelines)
|
||||
- [Submitting pull requests](https://github.com/XFox111/PasswordGeneratorExtension/blob/master/CONTRIBUTING.md#submitting-pull-requests)
|
||||
- [Finding an issue to work on](https://github.com/XFox111/PasswordGeneratorExtension/blob/master/CONTRIBUTING.md#finding-an-issue-to-work-on)
|
||||
- [Contributing to translations](https://github.com/XFox111/PasswordGeneratorExtension/blob/master/CONTRIBUTING.md#contributing-to-translations)
|
||||
If you are interested in fixing issues and contributing directly to the code base, please refer to the [Contribution Guidelines](https://github.com/XFox111/PasswordGeneratorExtension/wiki/Contribution-Guidelines)
|
||||
|
||||
## Code of Conduct
|
||||
This project has adopted the Contributor Covenant. For more information see the [Code of Conduct](https://github.com/XFox111/PasswordGeneratorExtension/blob/master/CODE_OF_CONDUCT.md)
|
||||
---
|
||||
|
||||
## Copyrights
|
||||
> ©2022 Eugene Fox
|
||||
[](https://bsky.app/profile/xfox111.net)
|
||||
[](https://github.com/xfox111)
|
||||
[](https://buymeacoffee.com/xfox111)
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
> ©2025 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/PasswordGeneratorExtension/blob/main/LICENSE)
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
Password generator extension has a linear versioning system. The latest version is always the most secure one. If you are using an older version, please update it to the latest one.
|
||||
Password generator extension has a linear versioning system. The latest version is always the most secure one. This is applied to major versions as well. If you are using an older version, please update it to the latest one.
|
||||
|
||||
We regularly run security audits and fix any security issues that are found. If you find a security issue, please report it to us as described below.
|
||||
|
||||
### 1.x EoL
|
||||
With 2.0 release bringing substantial changes to the extension, 1.x versions are no longer supported. Please update to the latest version.
|
||||
|
||||
If you are willing to continue using 1.x version, you can sideload it from the [releases page](https://github.com/XFox111/PasswordGeneratorExtension/releases/tag/v1.3). Use outdated versions at your own risk.
|
||||
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.x.x | :white_check_mark: |
|
||||
| < 2.0.0 | :x: |
|
||||
If you are willing to continue using an older version, you can sideload it from the [releases page](https://github.com/XFox111/PasswordGeneratorExtension/releases). Use outdated versions at your own risk.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
In case you find a security issue, please report it to us by [creating an issue](https://github.com/XFox111/PasswordGeneratorExtension/issues/new?assignees=xfox111&labels=security+vulnerability&template=bug_report.md&title=)
|
||||
You can report a security issue by clicking "Report a vulnerabilty" button at the top-right of this page, or by going through [this link](https://github.com/XFox111/PasswordGeneratorExtension/security/advisories/new)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { CracoConfig, CracoContext } from "@craco/craco";
|
||||
import HtmlWebapckPlugin, { MinifyOptions } from "html-webpack-plugin";
|
||||
import { Configuration } from "webpack";
|
||||
|
||||
// Craco config file
|
||||
// Craco is used to separate content and background scripts from the main JS bundle
|
||||
const cracoConfig: CracoConfig =
|
||||
{
|
||||
webpack:
|
||||
{
|
||||
configure: (webpackConfig: Configuration, { env, paths }: CracoContext): Configuration =>
|
||||
{
|
||||
const isProduction: boolean = env === "production";
|
||||
|
||||
const config: Configuration =
|
||||
{
|
||||
...webpackConfig,
|
||||
entry:
|
||||
{
|
||||
main: paths.appIndexJs,
|
||||
background: "./src/Services/BackgroundService.ts",
|
||||
contentScript: "./src/Services/ContentService.ts",
|
||||
},
|
||||
output:
|
||||
{
|
||||
...webpackConfig.output,
|
||||
filename: "static/js/[name].js"
|
||||
},
|
||||
optimization:
|
||||
{
|
||||
...webpackConfig.optimization,
|
||||
splitChunks: { cacheGroups: { default: false } },
|
||||
runtimeChunk: false
|
||||
}
|
||||
};
|
||||
|
||||
const minifyOptions: MinifyOptions =
|
||||
{
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true
|
||||
};
|
||||
|
||||
config.plugins = config.plugins?.filter((plugin: any) => plugin.constructor.name !== "HtmlWebpackPlugin") ?? [];
|
||||
|
||||
config.plugins.push(new HtmlWebapckPlugin({
|
||||
inject: true,
|
||||
chunks: ["main"],
|
||||
template: paths.appHtml,
|
||||
filename: "index.html",
|
||||
minify: isProduction && minifyOptions
|
||||
}));
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default cracoConfig;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
height: "100vh",
|
||||
gap: tokens.spacingHorizontalXL,
|
||||
padding: `${tokens.spacingVerticalXL} ${tokens.spacingHorizontalXL}`,
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
smallRoot:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column-reverse",
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalS}`,
|
||||
},
|
||||
hideScroll:
|
||||
{
|
||||
overflowY: "visible",
|
||||
maxHeight: "unset",
|
||||
},
|
||||
configRoot:
|
||||
{
|
||||
maxHeight: "90vh",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalM,
|
||||
width: "100%",
|
||||
maxWidth: "480px",
|
||||
borderRadius: tokens.borderRadiusLarge,
|
||||
boxShadow: tokens.shadow4,
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
|
||||
margin: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
|
||||
boxSizing: "border-box",
|
||||
justifySelf: "center",
|
||||
alignSelf: "center",
|
||||
overflowY: "auto",
|
||||
},
|
||||
switch:
|
||||
{
|
||||
margin: `${tokens.spacingVerticalXL} ${tokens.spacingVerticalNone}`,
|
||||
},
|
||||
listRoot:
|
||||
{
|
||||
width: "100%",
|
||||
maxWidth: "840px",
|
||||
alignSelf: "center",
|
||||
justifySelf: "center",
|
||||
padding: `${tokens.spacingVerticalXL} ${tokens.spacingHorizontalXL}`,
|
||||
boxSizing: "border-box",
|
||||
overflowY: "auto",
|
||||
maxHeight: "100%",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import DoubleLabledSwitch from "@/shared/DoubleLabeledSwitch";
|
||||
import { mergeClasses, Toaster } from "@fluentui/react-components";
|
||||
import { ReactElement } from "react";
|
||||
import { useMediaQuery } from "react-responsive";
|
||||
import PasswordList from "./components/PasswordList";
|
||||
import { useStyles } from "./Page.styles";
|
||||
import PassphraseSection from "./sections/PassphraseSection";
|
||||
import PasswordSection from "./sections/PasswordSection";
|
||||
|
||||
export default function Page(): ReactElement
|
||||
{
|
||||
const [isPassphrase, setIsPassphrase] = useState<boolean | null>(null);
|
||||
const [passwords, setPasswords] = useState<string[]>([]);
|
||||
const isSmall = useMediaQuery({ query: "(max-width: 1000px)" });
|
||||
|
||||
const cls = useStyles();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
advancedPassphraseSelected.getValue().then(setIsPassphrase);
|
||||
const unwatch = advancedPassphraseSelected.watch(setIsPassphrase);
|
||||
|
||||
return () => unwatch();
|
||||
}, []);
|
||||
|
||||
if (isPassphrase === null)
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<main className={ mergeClasses(cls.root, isSmall && cls.smallRoot) }>
|
||||
<PasswordList
|
||||
passwords={ passwords }
|
||||
className={ mergeClasses(cls.listRoot, isSmall && cls.hideScroll) } />
|
||||
|
||||
<article className={ mergeClasses(cls.configRoot, isSmall && cls.hideScroll) }>
|
||||
<DoubleLabledSwitch outerRoot={ { className: cls.switch } }
|
||||
checked={ isPassphrase }
|
||||
onChange={ (_, e) => advancedPassphraseSelected.setValue(e.checked) }
|
||||
offLabel={ i18n.t("advanced.password.title") }
|
||||
onLabel={ i18n.t("advanced.passphrase.title") } />
|
||||
|
||||
{ isPassphrase ?
|
||||
<PassphraseSection onGenerated={ setPasswords } />
|
||||
:
|
||||
<PasswordSection onGenerated={ setPasswords } />
|
||||
}
|
||||
</article>
|
||||
<Toaster />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export type GeneratorProps =
|
||||
{
|
||||
onGenerated: (passwords: string[]) => void;
|
||||
};
|
||||
|
||||
const advancedPassphraseSelected = storage.defineItem<boolean>("sync:AdvancedPassphraseSelected", { fallback: false });
|
||||
@@ -0,0 +1,22 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalXL,
|
||||
},
|
||||
actionRoot:
|
||||
{
|
||||
display: "flex",
|
||||
gap: tokens.spacingHorizontalM,
|
||||
alignSelf: "center",
|
||||
},
|
||||
bulkRoot:
|
||||
{
|
||||
display: "grid",
|
||||
alignItems: "center",
|
||||
gridTemplateColumns: "24px 24px 56px",
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Button, Input, InputOnChangeData, MessageBar, MessageBarBody, MessageBarTitle, Text, Toast, ToastTitle, useToastController } from "@fluentui/react-components";
|
||||
import { bundleIcon, Key24Regular, Save20Filled, Save20Regular } from "@fluentui/react-icons";
|
||||
import { PropsWithChildren, ReactElement } from "react";
|
||||
import { useStyles } from "./GeneratorForm.styles";
|
||||
|
||||
export default function GeneratorForm(props: GeneratorFormProps): ReactElement
|
||||
{
|
||||
const [passwordCount, private_setPasswordCount] = useState<number | null>(5);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const toaster = useToastController();
|
||||
|
||||
const cls = useStyles();
|
||||
const SaveIcon = bundleIcon(Save20Filled, Save20Regular);
|
||||
|
||||
const setPasswordCount = useCallback((_: any, e: InputOnChangeData) =>
|
||||
{
|
||||
const n = parseInt(e.value ?? "1");
|
||||
private_setPasswordCount(isNaN(n) || n < 1 ? null : Math.min(n, 1000));
|
||||
}, []);
|
||||
|
||||
const onSubmit = useCallback((args: React.FormEvent<HTMLFormElement>) =>
|
||||
{
|
||||
args.preventDefault();
|
||||
|
||||
try
|
||||
{
|
||||
setError(null);
|
||||
props.onGenerate(passwordCount ?? 1);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
setError((ex as Error).message);
|
||||
}
|
||||
}, [props.onGenerate, passwordCount]);
|
||||
|
||||
const onSave = useCallback(async () =>
|
||||
{
|
||||
props.onSave();
|
||||
await browser.storage.sync.set({ AdvancedBulkCount: passwordCount ?? 5 });
|
||||
|
||||
toaster.dispatchToast(
|
||||
<Toast>
|
||||
<ToastTitle>{ i18n.t("advanced.saved_msg") }</ToastTitle>
|
||||
</Toast>,
|
||||
{
|
||||
intent: "success",
|
||||
timeout: 1000
|
||||
}
|
||||
);
|
||||
}, [props.onSave, toaster, passwordCount]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
browser.storage.sync.get("AdvancedBulkCount").then(({ AdvancedBulkCount }) =>
|
||||
private_setPasswordCount(AdvancedBulkCount as number ?? 5)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form onSubmit={ onSubmit } className={ cls.root }>
|
||||
{ props.children }
|
||||
|
||||
{ error &&
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>{ error }</MessageBarTitle>
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
}
|
||||
|
||||
<div className={ cls.actionRoot }>
|
||||
<div className={ cls.bulkRoot }>
|
||||
<Key24Regular />
|
||||
<Text align="center">x</Text>
|
||||
<Input value={ passwordCount?.toString() ?? "" } onChange={ setPasswordCount } />
|
||||
</div>
|
||||
<Button appearance="primary" type="submit">{ i18n.t("advanced.actions.generate") }</Button>
|
||||
</div>
|
||||
|
||||
<Button appearance="subtle" icon={ <SaveIcon /> } onClick={ onSave }>
|
||||
{ i18n.t("advanced.actions.save_config") }
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export type GeneratorFormProps = PropsWithChildren &
|
||||
{
|
||||
onSave: () => void;
|
||||
onGenerate: (count: number) => void;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalS,
|
||||
},
|
||||
copyAll:
|
||||
{
|
||||
alignSelf: "flex-end",
|
||||
minHeight: "32px",
|
||||
},
|
||||
table:
|
||||
{
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
borderRadius: "16px",
|
||||
overflow: "clip",
|
||||
},
|
||||
row:
|
||||
{
|
||||
"&:last-child":
|
||||
{
|
||||
borderBottom: "none",
|
||||
}
|
||||
},
|
||||
cell:
|
||||
{
|
||||
padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
|
||||
cursor: "pointer",
|
||||
},
|
||||
cellLayout:
|
||||
{
|
||||
overflowX: "auto",
|
||||
},
|
||||
passwordText:
|
||||
{
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
overflowX: "hidden",
|
||||
display: "block",
|
||||
marginRight: "36px",
|
||||
},
|
||||
copyIcon:
|
||||
{
|
||||
verticalAlign: "middle",
|
||||
padding: tokens.spacingHorizontalXL,
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { bundleIcon, Copy32Regular, CopyFilled, CopyRegular } from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import { useStyles } from "./PasswordList.styles";
|
||||
|
||||
export default function PasswordList({ passwords, className }: PasswordListProps): ReactElement
|
||||
{
|
||||
const toaster = fui.useToastController();
|
||||
|
||||
const CopyIcon = bundleIcon(CopyFilled, CopyRegular);
|
||||
const cls = useStyles();
|
||||
|
||||
const copy = (password?: string) =>
|
||||
{
|
||||
navigator.clipboard.writeText(password ?? passwords.join("\n"));
|
||||
toaster.dispatchToast(
|
||||
<fui.Toast>
|
||||
<fui.ToastTitle>{ i18n.t("advanced.copied_msg") }</fui.ToastTitle>
|
||||
</fui.Toast>,
|
||||
{
|
||||
intent: "success",
|
||||
timeout: 1000
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ fui.mergeClasses(cls.root, className) }>
|
||||
{ passwords.length > 0 &&
|
||||
<>
|
||||
<fui.Button className={ cls.copyAll }
|
||||
appearance="subtle" icon={ <CopyIcon /> }
|
||||
onClick={ () => copy() }>
|
||||
|
||||
{ i18n.t("advanced.actions.copy_all") }
|
||||
</fui.Button>
|
||||
|
||||
<fui.Table className={ cls.table }>
|
||||
<fui.TableBody>
|
||||
|
||||
{ passwords.map((password, index) =>
|
||||
<fui.TableRow key={ index } className={ cls.row }
|
||||
onClick={ () => copy(password) }>
|
||||
|
||||
<fui.TableCell className={ cls.cell }>
|
||||
|
||||
<fui.TableCellLayout content={ { className: cls.cellLayout } }>
|
||||
<fui.Tooltip relationship="description" content={ password }>
|
||||
|
||||
<fui.Text className={ cls.passwordText }
|
||||
font="monospace" size={ 600 }>
|
||||
|
||||
{ password }
|
||||
</fui.Text>
|
||||
|
||||
</fui.Tooltip>
|
||||
</fui.TableCellLayout>
|
||||
|
||||
<fui.TableCellActions>
|
||||
<Copy32Regular className={ cls.copyIcon } />
|
||||
</fui.TableCellActions>
|
||||
|
||||
</fui.TableCell>
|
||||
</fui.TableRow>
|
||||
) }
|
||||
|
||||
</fui.TableBody>
|
||||
</fui.Table>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type PasswordListProps =
|
||||
{
|
||||
className?: string;
|
||||
passwords: string[];
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Advanced password generator</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,51 @@
|
||||
html,
|
||||
body,
|
||||
#root > .fui-FluentProvider
|
||||
{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
p, h1, h2
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar
|
||||
{
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track
|
||||
{
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb
|
||||
{
|
||||
border-radius: 4px;
|
||||
background: light-dark(#d1d1d1, #666666);
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover
|
||||
{
|
||||
background: light-dark(#c7c7c7, #757575);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover:active
|
||||
{
|
||||
background: light-dark(#b3b3b3, #6b6b6b);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "../../shared/App";
|
||||
import "./main.css";
|
||||
import Page from "./Page";
|
||||
|
||||
document.title = i18n.t("advanced.title");
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App>
|
||||
<Page />
|
||||
</App>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalSNudge,
|
||||
},
|
||||
checkboxes:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalXXS,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import generatePassphrase, { PassphraseProps } from "@/utils/generators/generatePassphrase";
|
||||
import infoLabel from "@/utils/infoLabel";
|
||||
import { Checkbox, Field, Input, InputOnChangeData } from "@fluentui/react-components";
|
||||
import { ReactElement } from "react";
|
||||
import GeneratorForm from "../components/GeneratorForm";
|
||||
import { GeneratorProps } from "../Page";
|
||||
import { useStyles } from "./PassphraseSection.styles";
|
||||
|
||||
export default function PassphraseSection(props: GeneratorProps): ReactElement
|
||||
{
|
||||
const [wordCount, private_setWordCount] = useState<number | null>(2);
|
||||
const [swapCharacters, setSwapCharacters] = useState<boolean>(false);
|
||||
const [separate, setSeparate] = useState<boolean>(true);
|
||||
const [separator, setSeparator] = useState<string>("");
|
||||
const [allowRepeating, setAllowRepeating] = useState<boolean>(false);
|
||||
const [randomizeCase, setRandomizeCase] = useState<boolean>(false);
|
||||
|
||||
const config = useMemo<PassphraseProps>(() => ({
|
||||
wordCount: wordCount ?? 2,
|
||||
allowRepeating,
|
||||
swapCharacters,
|
||||
randomizeCase,
|
||||
separator: separate ? (separator ? separator : " ") : ""
|
||||
}), [wordCount, allowRepeating, swapCharacters, randomizeCase, separate, separator]);
|
||||
|
||||
const cls = useStyles();
|
||||
|
||||
const setWordCount = useCallback((_: any, e: InputOnChangeData) =>
|
||||
{
|
||||
const n = parseInt(e.value ?? "");
|
||||
private_setWordCount(isNaN(n) || n < 1 ? null : Math.min(n, 100));
|
||||
}, []);
|
||||
|
||||
const saveConfiguration = useCallback(
|
||||
async () => await browser.storage.sync.set({ AdvancedPassphraseOptions: config }),
|
||||
[config]
|
||||
);
|
||||
|
||||
const generate = useCallback((count: number) =>
|
||||
{
|
||||
const passwords: string[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++)
|
||||
passwords.push(generatePassphrase(config));
|
||||
|
||||
props.onGenerated(passwords);
|
||||
}, [config, props.onGenerated]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
browser.storage.sync.get("AdvancedPassphraseOptions").then(({ AdvancedPassphraseOptions }) =>
|
||||
{
|
||||
if (!AdvancedPassphraseOptions)
|
||||
return;
|
||||
|
||||
private_setWordCount(AdvancedPassphraseOptions.wordCount ?? 2);
|
||||
setAllowRepeating(AdvancedPassphraseOptions.allowRepeating);
|
||||
setSwapCharacters(AdvancedPassphraseOptions.swapCharacters);
|
||||
setRandomizeCase(AdvancedPassphraseOptions.randomizeCase);
|
||||
setSeparate(!!AdvancedPassphraseOptions.separator);
|
||||
setSeparator(AdvancedPassphraseOptions.separator ?? "");
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GeneratorForm onGenerate={ generate } onSave={ saveConfiguration }>
|
||||
<div className={ cls.root }>
|
||||
<Field label={ i18n.t("advanced.passphrase.length") }>
|
||||
<Input value={ wordCount?.toString() ?? "" } onChange={ setWordCount } />
|
||||
</Field>
|
||||
<div className={ cls.checkboxes }>
|
||||
<Checkbox label={ i18n.t("advanced.passphrase.replace") }
|
||||
checked={ swapCharacters } onChange={ (_, e) => setSwapCharacters(e.checked as boolean) } />
|
||||
<Checkbox label={ i18n.t("advanced.passphrase.random_case") }
|
||||
checked={ randomizeCase } onChange={ (_, e) => setRandomizeCase(e.checked as boolean) } />
|
||||
<Checkbox label={ infoLabel(i18n.t("advanced.passphrase.allow_repat.label"), i18n.t("advanced.passphrase.allow_repat.hint")) }
|
||||
checked={ allowRepeating } onChange={ (_, e) => setAllowRepeating(e.checked as boolean) } />
|
||||
|
||||
<div>
|
||||
<Checkbox label={ infoLabel(i18n.t("advanced.passphrase.separate_words.label"), i18n.t("advanced.passphrase.separate_words.hint")) }
|
||||
checked={ separate } onChange={ (_, e) => setSeparate(e.checked as boolean) } />
|
||||
<Input disabled={ !separate } size="small"
|
||||
value={ separator } onChange={ (_, e) => setSeparator(e.value) } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GeneratorForm>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { CharacterHints, generatePassword } from "@/utils/generators/generatePassword";
|
||||
import infoLabel from "@/utils/infoLabel";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { ReactElement } from "react";
|
||||
import { GeneratorProps } from "../Page";
|
||||
import GeneratorForm from "../components/GeneratorForm";
|
||||
|
||||
// TODO: needs refactoring
|
||||
export default function PasswordSection(props: GeneratorProps): ReactElement
|
||||
{
|
||||
const [state, private_setState] = useState<PasswordSectionState>({
|
||||
length: 8,
|
||||
enableUppercase: true, uppercaseCount: 1,
|
||||
enableLowercase: true, lowercaseCount: 1,
|
||||
enableNumeric: true, numericCount: 1,
|
||||
enableSpecial: true, specialCount: 1,
|
||||
enableCustom: false, customCount: 1, customSet: "",
|
||||
|
||||
excludeSimilar: true,
|
||||
excludeAmbiguous: true,
|
||||
excludeRepeating: false,
|
||||
excludeCustom: false, excludeCustomSet: ""
|
||||
});
|
||||
|
||||
const cls = useStyles();
|
||||
|
||||
const setState = useCallback((newState: Partial<PasswordSectionState>) =>
|
||||
{
|
||||
private_setState({ ...state, ...newState });
|
||||
}, [state]);
|
||||
|
||||
const setLength = useCallback((_: any, e: fui.InputOnChangeData) =>
|
||||
{
|
||||
const n = parseInt(e.value ?? "");
|
||||
setState({ length: isNaN(n) || n < 1 ? null : n });
|
||||
}, [state]);
|
||||
|
||||
const saveConfiguration = useCallback(
|
||||
async () => await browser.storage.sync.set({ AdvancedPasswordOptions: state }),
|
||||
[state]
|
||||
);
|
||||
|
||||
const generate = useCallback((count: number) =>
|
||||
{
|
||||
const passwords: string[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++)
|
||||
passwords.push(generatePassword({
|
||||
length: state.length ?? 8,
|
||||
custom: state.enableCustom ? state.customCount ?? 1 : 0,
|
||||
customSet: state.customSet,
|
||||
numeric: state.enableNumeric ? state.numericCount ?? 1 : 0,
|
||||
special: state.enableSpecial ? state.specialCount ?? 1 : 0,
|
||||
uppercase: state.enableUppercase ? state.uppercaseCount ?? 1 : 0,
|
||||
lowercase: state.enableLowercase ? state.lowercaseCount ?? 1 : 0,
|
||||
excludeAmbiguous: state.excludeAmbiguous,
|
||||
excludeCustom: state.excludeCustom ? state.excludeCustomSet : "",
|
||||
excludeRepeating: state.excludeRepeating,
|
||||
excludeSimilar: state.excludeSimilar,
|
||||
}));
|
||||
|
||||
props.onGenerated(passwords);
|
||||
}, [state, props.onGenerated]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
browser.storage.sync.get("AdvancedPasswordOptions").then(({ AdvancedPasswordOptions }) =>
|
||||
private_setState({ ...state, ...AdvancedPasswordOptions as PasswordSectionState }));
|
||||
}, []);
|
||||
|
||||
const checkboxControls = useCallback((key: keyof PasswordSectionState): Partial<fui.CheckboxProps> => ({
|
||||
checked: state[key] as boolean,
|
||||
onChange: (_, e) => setState({ [key]: e.checked as boolean })
|
||||
}), [state]);
|
||||
|
||||
const minInputControls = useCallback((enabledKey: keyof PasswordSectionState, key: keyof PasswordSectionState): Partial<fui.InputProps> => ({
|
||||
size: "small",
|
||||
disabled: !state[enabledKey],
|
||||
value: state[key]?.toString() ?? "",
|
||||
onChange: (_, e) => setState({ [key]: parseCount(e.value) })
|
||||
}), [state]);
|
||||
|
||||
return (
|
||||
<GeneratorForm onGenerate={ generate } onSave={ saveConfiguration }>
|
||||
<fui.Field label={ i18n.t("advanced.password.length") }>
|
||||
<fui.Input value={ state.length?.toString() ?? "" } onChange={ setLength } />
|
||||
</fui.Field>
|
||||
<fui.Table size="small" as="div">
|
||||
<fui.TableHeader as="div">
|
||||
<fui.TableRow as="div">
|
||||
<fui.TableHeaderCell as="div">{ i18n.t("common.sections.include") }</fui.TableHeaderCell>
|
||||
<fui.TableHeaderCell as="div">{ i18n.t("advanced.password.min_of_type") }</fui.TableHeaderCell>
|
||||
</fui.TableRow>
|
||||
</fui.TableHeader>
|
||||
<fui.TableBody as="div">
|
||||
<Row>
|
||||
<fui.Checkbox label={ i18n.t("common.characters.uppercase") } { ...checkboxControls("enableUppercase") } />
|
||||
<fui.Input { ...minInputControls("enableUppercase", "uppercaseCount") } />
|
||||
</Row>
|
||||
<Row>
|
||||
<fui.Checkbox label={ i18n.t("common.characters.lowercase") } { ...checkboxControls("enableLowercase") } />
|
||||
<fui.Input { ...minInputControls("enableLowercase", "lowercaseCount") } />
|
||||
</Row>
|
||||
<Row>
|
||||
<fui.Checkbox label={ i18n.t("common.characters.numeric") } { ...checkboxControls("enableNumeric") } />
|
||||
<fui.Input { ...minInputControls("enableNumeric", "numericCount") } />
|
||||
</Row>
|
||||
<Row>
|
||||
<fui.Checkbox label={ infoLabel(i18n.t("common.characters.special"), CharacterHints.special) } { ...checkboxControls("enableSpecial") } />
|
||||
<fui.Input { ...minInputControls("enableSpecial", "specialCount") } />
|
||||
</Row>
|
||||
<Row>
|
||||
<>
|
||||
<fui.Checkbox { ...checkboxControls("enableCustom") } />
|
||||
<fui.Input size="small" disabled={ !state.enableCustom }
|
||||
placeholder={ i18n.t("common.characters.custom") }
|
||||
value={ state.customSet } onChange={ (_, e) => setState({ customSet: e.value }) } />
|
||||
</>
|
||||
<fui.Input { ...minInputControls("enableCustom", "customCount") } />
|
||||
</Row>
|
||||
</fui.TableBody>
|
||||
</fui.Table>
|
||||
|
||||
<section className={ cls.section }>
|
||||
<fui.Text>{ i18n.t("common.sections.exclude") }</fui.Text>
|
||||
<fui.Checkbox label={ infoLabel(i18n.t("common.characters.similar"), CharacterHints.similar) } { ...checkboxControls("excludeSimilar") } />
|
||||
<fui.Checkbox label={ infoLabel(i18n.t("common.characters.ambiguous"), CharacterHints.ambiguous) } disabled={ !state.enableSpecial } { ...checkboxControls("excludeAmbiguous") } />
|
||||
<fui.Checkbox label={ infoLabel(i18n.t("common.characters.repeating.label"), i18n.t("common.characters.repeating.hint")) } { ...checkboxControls("excludeRepeating") } />
|
||||
<div>
|
||||
<fui.Checkbox { ...checkboxControls("excludeCustom") } />
|
||||
<fui.Input size="small" disabled={ !state.excludeCustom }
|
||||
placeholder={ i18n.t("common.characters.custom") }
|
||||
value={ state.excludeCustomSet } onChange={ (_, e) => setState({ excludeCustomSet: e.value }) } />
|
||||
</div>
|
||||
</section>
|
||||
</GeneratorForm>
|
||||
);
|
||||
}
|
||||
|
||||
function parseCount(value: string): number | null
|
||||
{
|
||||
const n = parseInt(value);
|
||||
return isNaN(n) || n < 1 ? null : Math.min(n, 100);
|
||||
};
|
||||
|
||||
function Row(props: { children: ReactElement[]; }): ReactElement
|
||||
{
|
||||
return (
|
||||
<fui.TableRow as="div">
|
||||
{ props.children.map((i, index) =>
|
||||
<fui.TableCell key={ index } as="div">
|
||||
{ i }
|
||||
</fui.TableCell>
|
||||
) }
|
||||
</fui.TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = fui.makeStyles({
|
||||
section:
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
});
|
||||
|
||||
type PasswordSectionState =
|
||||
{
|
||||
length: number | null;
|
||||
enableUppercase: boolean;
|
||||
uppercaseCount: number | null;
|
||||
enableLowercase: boolean;
|
||||
lowercaseCount: number | null;
|
||||
enableNumeric: boolean;
|
||||
numericCount: number | null;
|
||||
enableSpecial: boolean;
|
||||
specialCount: number | null;
|
||||
enableCustom: boolean;
|
||||
customCount: number | null;
|
||||
|
||||
excludeSimilar: boolean;
|
||||
excludeAmbiguous: boolean;
|
||||
excludeRepeating: boolean;
|
||||
excludeCustom: boolean;
|
||||
|
||||
excludeCustomSet: string;
|
||||
customSet: string;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<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>
|
||||
|
||||
</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,51 +1,38 @@
|
||||
{
|
||||
"name": "password-generator",
|
||||
"version": "2.0.2",
|
||||
"version": "5.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"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": {
|
||||
"@craco/craco": "^6.4.5",
|
||||
"@fluentui/react-components": "^9.5.1",
|
||||
"@fluentui/react-icons": "^2.0.185",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.55.0",
|
||||
"typescript": "^4.8.4",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
"@fluentui/react-components": "^9.58.2",
|
||||
"@fluentui/react-icons": "^2.0.274",
|
||||
"@wxt-dev/i18n": "^0.2.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-responsive": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/craco__craco": "^6.4.0",
|
||||
"@types/jest": "^29.1.2",
|
||||
"@types/node": "^18.11.0",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/webextension-polyfill": "^0.9.1",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.23.0",
|
||||
"@typescript-eslint/parser": "^8.23.0",
|
||||
"@wxt-dev/module-react": "^1.1.3",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"globals": "^15.14.0",
|
||||
"typescript": "^5.7.3",
|
||||
"wxt": "^0.19.26"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
{
|
||||
"name": {
|
||||
"message": "Password Generator",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"description": {
|
||||
"message": "Password generator extension allows you to easily generate long and secure password in one click",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"author": {
|
||||
"message": "Eugene Fox",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"Password_generator": {
|
||||
"message": "Password generator",
|
||||
"description": "App.tsx"
|
||||
},
|
||||
"Copy": {
|
||||
"message": "Copy",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Generate_new": {
|
||||
"message": "Generate new",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Exclude_special_symbols_one_time": {
|
||||
"message": "Generate password without special symbols",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Include_special_symbols_one_time": {
|
||||
"message": "Generate password with special symbols",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Settings": {
|
||||
"message": "Settings",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Password_length": {
|
||||
"message": "Password length",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Recommended_password_length": {
|
||||
"message": "Recommended password length",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Character_options": {
|
||||
"message": "Character options",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Include": {
|
||||
"message": "Include",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Special_symbols": {
|
||||
"message": "Special symbols",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Numeric": {
|
||||
"message": "Numeric",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Uppercase": {
|
||||
"message": "Uppercase",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Lowercase": {
|
||||
"message": "Lowercase",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Exclude": {
|
||||
"message": "Exclude",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Similar": {
|
||||
"message": "Similar",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Ambiguous": {
|
||||
"message": "Ambiguous",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Repeating": {
|
||||
"message": "Repeating",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Add_shortcut_to_context_menu": {
|
||||
"message": "Add shortcut to context menu",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Right_click_password_field_to_quickly_generate_password": {
|
||||
"message": "Right-click password field to quickly generate password",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Automatically_copy_to_clipboard": {
|
||||
"message": "Automatically copy to clipboard",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"About": {
|
||||
"message": "About",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Developed_by_Eugene_Fox": {
|
||||
"message": "Developed by Eugene Fox",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Licensed_under": {
|
||||
"message": "Licensed under",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"MIT_license": {
|
||||
"message": "MIT license",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Want_to_contribute_translation_for_your_language_": {
|
||||
"message": "Want to contribute translation for your language?",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Read_this_to_get_started": {
|
||||
"message": "Read this to get started",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"My_website": {
|
||||
"message": "My website",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Source_code": {
|
||||
"message": "Source code",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Changelog": {
|
||||
"message": "Changelog",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Leave_feedback": {
|
||||
"message": "Leave feedback",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Buy_me_a_coffee": {
|
||||
"message": "Buy me a coffee",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Set_name": {
|
||||
"message": "Name",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"Characters": {
|
||||
"message": "Characters",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"__etc_": {
|
||||
"message": ", etc.",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"OK": {
|
||||
"message": "OK",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"Either_lowercase_or_uppercase_characters_must_be_included": {
|
||||
"message": "Either lowercase or uppercase characters must be included",
|
||||
"description": "Generator.tsx"
|
||||
},
|
||||
"Selected_length_is_too_long_to_exclude_repeating_characters": {
|
||||
"message": "Selected length is too long to exclude repeating characters",
|
||||
"description": "Generator.tsx"
|
||||
},
|
||||
"Quick_generator_is_only_available_on_password_fields": {
|
||||
"message": "Quick generator is only available on password fields",
|
||||
"description": "ContentService.tsx"
|
||||
},
|
||||
"Quick_generate_password": {
|
||||
"message": "Quick generate password",
|
||||
"description": "BackgroundService.tsx"
|
||||
}
|
||||
}
|
||||
@@ -1,174 +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"
|
||||
},
|
||||
"Password_generator": {
|
||||
"message": "Generator haseł",
|
||||
"description": "App.tsx"
|
||||
},
|
||||
"Copy": {
|
||||
"message": "Kopiuj",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Generate_new": {
|
||||
"message": "Utwórz nowy",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Exclude_special_symbols_one_time": {
|
||||
"message": "Wygeneruj hasło bez znaków specjalnych",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Include_special_symbols_one_time": {
|
||||
"message": "Wygeneruj hasło z znakami specjalnymi",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Settings": {
|
||||
"message": "Ustawienia",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Password_length": {
|
||||
"message": "Długość hasła",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Recommended_password_length": {
|
||||
"message": "Zalecana długość hasła",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Character_options": {
|
||||
"message": "Ustawienia symboli",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Include": {
|
||||
"message": "Włącz",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Special_symbols": {
|
||||
"message": "Znaki specjalne",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Numeric": {
|
||||
"message": "Liczby",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Uppercase": {
|
||||
"message": "Wielkie litery",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Lowercase": {
|
||||
"message": "Małe litery",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Exclude": {
|
||||
"message": "Wyłącz",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Similar": {
|
||||
"message": "Podobne",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Ambiguous": {
|
||||
"message": "Niebezpieczne",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Repeating": {
|
||||
"message": "Powtarzające się",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Add_shortcut_to_context_menu": {
|
||||
"message": "Dodaj rozszerzenie do menu kontekstowego",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Right_click_password_field_to_quickly_generate_password": {
|
||||
"message": "Kliknij prawym przyciskiem myszy w pole hasła, aby szybko wygenerować hasło",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Automatically_copy_to_clipboard": {
|
||||
"message": "Automatycznie kopiuj do schowka",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"About": {
|
||||
"message": "O rozszerzeniu",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Developed_by_Eugene_Fox": {
|
||||
"message": "Autor: Jewgienij Lis",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Licensed_under": {
|
||||
"message": "Licencja",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"MIT_license": {
|
||||
"message": "MIT",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Want_to_contribute_translation_for_your_language_": {
|
||||
"message": "Chcesz pomóc z tłumaczeniem na swój język?",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Read_this_to_get_started": {
|
||||
"message": "Przeczytaj ten artykuł",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"My_website": {
|
||||
"message": "Moja strona internetowa",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Source_code": {
|
||||
"message": "Kod źródłowy",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Changelog": {
|
||||
"message": "Lista zmian",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Leave_feedback": {
|
||||
"message": "Zostaw opinię",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Buy_me_a_coffee": {
|
||||
"message": "Wesprzyj",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Set_name": {
|
||||
"message": "Nazwa",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"Characters": {
|
||||
"message": "Znaki",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"__etc_": {
|
||||
"message": " itp.",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"OK": {
|
||||
"message": "OK",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"Either_lowercase_or_uppercase_characters_must_be_included": {
|
||||
"message": "Muszą być uwzględnione małe lub wielkie litery",
|
||||
"description": "Generator.tsx"
|
||||
},
|
||||
"Selected_length_is_too_long_to_exclude_repeating_characters": {
|
||||
"message": "Wybrana długość jest zbyt długa, aby wykluczyć powtarzające się znaki",
|
||||
"description": "Generator.tsx"
|
||||
},
|
||||
"Quick_generator_is_only_available_on_password_fields": {
|
||||
"message": "Szybki generator jest dostępny tylko dla pól hasła",
|
||||
"description": "ContentService.tsx"
|
||||
},
|
||||
"Quick_generate_password": {
|
||||
"message": "Wygeneruj hasło",
|
||||
"description": "BackgroundService.tsx"
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
{
|
||||
"name": {
|
||||
"message": "Генератор паролей",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"description": {
|
||||
"message": "Расширение, позволяющее легко генерировать сложные и надежные пароли в один клик",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"author": {
|
||||
"message": "Евгений Лис",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"Password_generator": {
|
||||
"message": "Генератор паролей",
|
||||
"description": "App.tsx"
|
||||
},
|
||||
"Copy": {
|
||||
"message": "Копировать",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Generate_new": {
|
||||
"message": "Создать новый",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Exclude_special_symbols_one_time": {
|
||||
"message": "Сгенерировать пароль без спецсимволов",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Include_special_symbols_one_time": {
|
||||
"message": "Сгенерировать пароль со спецсимволами",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Settings": {
|
||||
"message": "Настройки",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Password_length": {
|
||||
"message": "Длина пароля",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Recommended_password_length": {
|
||||
"message": "Рекомендуемая длина пароля",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Character_options": {
|
||||
"message": "Настройки символов",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Include": {
|
||||
"message": "Включить",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Special_symbols": {
|
||||
"message": "Специальные символы",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Numeric": {
|
||||
"message": "Цифры",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Uppercase": {
|
||||
"message": "Прописные буквы",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Lowercase": {
|
||||
"message": "Строчные буквы",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Exclude": {
|
||||
"message": "Исключить",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Similar": {
|
||||
"message": "Похожие",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Ambiguous": {
|
||||
"message": "Особые",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Repeating": {
|
||||
"message": "Повторяющиеся",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Add_shortcut_to_context_menu": {
|
||||
"message": "Добавить расширение в контекстное меню",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Right_click_password_field_to_quickly_generate_password": {
|
||||
"message": "Щелкните правой кнопкой мыши по полю ввода пароля, чтобы быстро сгенерировать пароль",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Automatically_copy_to_clipboard": {
|
||||
"message": "Автоматически копировать в буфер обмена",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"About": {
|
||||
"message": "О расширении",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Developed_by_Eugene_Fox": {
|
||||
"message": "Разработчик Евгений Лис",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Licensed_under": {
|
||||
"message": "Лицензия",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"MIT_license": {
|
||||
"message": "MIT",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Want_to_contribute_translation_for_your_language_": {
|
||||
"message": "Хотите помочь с переводом на свой язык?",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Read_this_to_get_started": {
|
||||
"message": "Прочтите эту статью",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"My_website": {
|
||||
"message": "Мой сайт",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Source_code": {
|
||||
"message": "Исходный код",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Changelog": {
|
||||
"message": "Список изменений",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Leave_feedback": {
|
||||
"message": "Оставить отзыв",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Buy_me_a_coffee": {
|
||||
"message": "Спонсировать",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Set_name": {
|
||||
"message": "Название",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"Characters": {
|
||||
"message": "Символы",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"__etc_": {
|
||||
"message": " и т.д.",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"OK": {
|
||||
"message": "ОК",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"Either_lowercase_or_uppercase_characters_must_be_included": {
|
||||
"message": "Должны быть включены строчные или прописные буквы",
|
||||
"description": "Generator.tsx"
|
||||
},
|
||||
"Selected_length_is_too_long_to_exclude_repeating_characters": {
|
||||
"message": "Выбранная длина слишком велика для исключения повторяющихся символов",
|
||||
"description": "Generator.tsx"
|
||||
},
|
||||
"Quick_generator_is_only_available_on_password_fields": {
|
||||
"message": "Быстрый генератор доступен только для полей ввода пароля",
|
||||
"description": "ContentService.tsx"
|
||||
},
|
||||
"Quick_generate_password": {
|
||||
"message": "Сгенерировать пароль",
|
||||
"description": "BackgroundService.tsx"
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
{
|
||||
"name": {
|
||||
"message": "Генератор паролів",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"description": {
|
||||
"message": "Розширення, яке дозволяє легко генерувати складні та надійні паролі в один клік",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"author": {
|
||||
"message": "Євген Лис",
|
||||
"description": "manifest.json"
|
||||
},
|
||||
"Password_generator": {
|
||||
"message": "Генератор паролів",
|
||||
"description": "App.tsx"
|
||||
},
|
||||
"Copy": {
|
||||
"message": "Копіювати",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Generate_new": {
|
||||
"message": "Генерувати новий",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Exclude_special_symbols_one_time": {
|
||||
"message": "Генерувати пароль без спеціальних символів",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Include_special_symbols_one_time": {
|
||||
"message": "Генерувати пароль з спеціальними символами",
|
||||
"description": "PasswordView.tsx"
|
||||
},
|
||||
"Settings": {
|
||||
"message": "Налаштування",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Password_length": {
|
||||
"message": "Довжина паролю",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Recommended_password_length": {
|
||||
"message": "Рекомендована довжина паролю",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Character_options": {
|
||||
"message": "Параметри символів",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Include": {
|
||||
"message": "Включити",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Special_symbols": {
|
||||
"message": "Спеціальні символи",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Numeric": {
|
||||
"message": "Цифри",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Uppercase": {
|
||||
"message": "Великі літери",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Lowercase": {
|
||||
"message": "Малі літери",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Exclude": {
|
||||
"message": "Виключити",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Similar": {
|
||||
"message": "Схожі",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Ambiguous": {
|
||||
"message": "Особливі",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Repeating": {
|
||||
"message": "Повторювані",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Add_shortcut_to_context_menu": {
|
||||
"message": "Додати розширення до контекстного меню",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Right_click_password_field_to_quickly_generate_password": {
|
||||
"message": "Правий клік на поле вводу паролю для швидкого генерування паролю",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"Automatically_copy_to_clipboard": {
|
||||
"message": "Автоматично копіювати в буфер обміну",
|
||||
"description": "SettingsSection.tsx"
|
||||
},
|
||||
"About": {
|
||||
"message": "Про розширення",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Developed_by_Eugene_Fox": {
|
||||
"message": "Розроблено Євгеном Лисом",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Licensed_under": {
|
||||
"message": "Ліцензовано під",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"MIT_license": {
|
||||
"message": "MIT",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Want_to_contribute_translation_for_your_language_": {
|
||||
"message": "Хочете допомогти перекласти розширення на свою мову?",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Read_this_to_get_started": {
|
||||
"message": "Прочитайте цю статтю",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"My_website": {
|
||||
"message": "Моя веб-сторінка",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Source_code": {
|
||||
"message": "Вихідний код",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Changelog": {
|
||||
"message": "Список змін",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Leave_feedback": {
|
||||
"message": "Залишити відгук",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Buy_me_a_coffee": {
|
||||
"message": "Підтримати",
|
||||
"description": "AboutSection.tsx"
|
||||
},
|
||||
"Set_name": {
|
||||
"message": "Назва",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"Characters": {
|
||||
"message": "Символи",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"__etc_": {
|
||||
"message": " і т.д.",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"OK": {
|
||||
"message": "OK",
|
||||
"description": "CharacterHelpDialog.tsx"
|
||||
},
|
||||
"Either_lowercase_or_uppercase_characters_must_be_included": {
|
||||
"message": "Повинні бути включені малі або великі літери",
|
||||
"description": "Generator.tsx"
|
||||
},
|
||||
"Selected_length_is_too_long_to_exclude_repeating_characters": {
|
||||
"message": "Вибрана довжина занадто велика для виключення повторюваних символів",
|
||||
"description": "Generator.tsx"
|
||||
},
|
||||
"Quick_generator_is_only_available_on_password_fields": {
|
||||
"message": "Швидкий генератор доступний тільки для полів вводу паролів",
|
||||
"description": "ContentService.tsx"
|
||||
},
|
||||
"Quick_generate_password": {
|
||||
"message": "Згенерувати пароль",
|
||||
"description": "BackgroundService.tsx"
|
||||
}
|
||||
}
|
||||
|
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 |
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Password Generator</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_name__",
|
||||
"description": "__MSG_description__",
|
||||
"author": "__MSG_author__",
|
||||
"version": "2.0.0",
|
||||
"default_locale": "en",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"contextMenus",
|
||||
"clipboardWrite"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "./static/js/background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"./static/js/contentScript.js"
|
||||
],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "index.html",
|
||||
"default_title": "__MSG_name__"
|
||||
},
|
||||
"icons": {
|
||||
"128": "icons/icon-128.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"32": "icons/icon-32.png",
|
||||
"16": "icons/icon-16.png"
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_name__",
|
||||
"description": "__MSG_description__",
|
||||
"author": "__MSG_author__",
|
||||
"version": "2.0.0",
|
||||
"default_locale": "en",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"contextMenus",
|
||||
"clipboardWrite"
|
||||
],
|
||||
"background": {
|
||||
"scripts": [
|
||||
"./static/js/background.js"
|
||||
],
|
||||
"persistent": true
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"./static/js/contentScript.js"
|
||||
],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"browser_action": {
|
||||
"default_popup": "index.html",
|
||||
"default_title": "__MSG_name__"
|
||||
},
|
||||
"icons": {
|
||||
"128": "icons/icon-128.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"32": "icons/icon-32.png",
|
||||
"16": "icons/icon-16.png"
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "passwordgenerator@xfox111.net",
|
||||
"strict_min_version": "58.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { GriffelStyle, makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
const random = (max: number): number => Math.floor(Math.random() * max);
|
||||
|
||||
export const SNOWFLAKES_NUM: number = 100;
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
snow:
|
||||
{
|
||||
position: "absolute",
|
||||
overflow: "hidden",
|
||||
pointerEvents: "none",
|
||||
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
snowflake:
|
||||
{
|
||||
"--size": "1px",
|
||||
width: "var(--size)",
|
||||
height: "var(--size)",
|
||||
backgroundColor: tokens.colorScrollbarOverlay,
|
||||
borderRadius: tokens.borderRadiusCircular,
|
||||
position: "absolute",
|
||||
top: "-5px",
|
||||
},
|
||||
...[...Array(SNOWFLAKES_NUM)].reduce(
|
||||
(acc, _, i): Record<string, GriffelStyle> => ({
|
||||
...acc,
|
||||
[`snowflake-${i}`]: {
|
||||
"--size": `${random(5)}px`,
|
||||
"--left-start": `${random(20) - 10}vw`,
|
||||
"--left-end": `${random(20) - 10}vw`,
|
||||
left: `${random(100)}vw`,
|
||||
animationName: "snowfall",
|
||||
animationDuration: `${5 + random(10)}s`,
|
||||
animationTimingFunction: "linear",
|
||||
animationIterationCount: "infinite",
|
||||
animationDelay: `-${random(10)}s`,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
@@ -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,124 +0,0 @@
|
||||
main
|
||||
{
|
||||
width: 400px;
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Buy me a coffee button style
|
||||
.fui-Button.bmc
|
||||
{
|
||||
background-color: var(--colorPaletteDarkOrangeBorder2) !important;
|
||||
|
||||
&:hover
|
||||
{
|
||||
background-color: var(--colorPaletteDarkOrangeForeground1) !important;
|
||||
}
|
||||
|
||||
&:active
|
||||
{
|
||||
background-color: var(--colorPaletteDarkOrangeBackground2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Overrides for default FluentUI styles
|
||||
a.fui-Button
|
||||
{
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.fui-Accordion
|
||||
{
|
||||
section
|
||||
{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.fui-AccordionHeader__expandIcon > svg
|
||||
{
|
||||
transition-duration: .5s;
|
||||
transition-timing-function: var(--curveDecelerateMax);
|
||||
}
|
||||
}
|
||||
|
||||
.scaleUpIn
|
||||
{
|
||||
animation-name: scaleUpInAnim;
|
||||
animation-timing-function: var(--curveEasyEaseMax);
|
||||
animation-duration: .5s;
|
||||
}
|
||||
|
||||
@keyframes scaleUpInAnim
|
||||
{
|
||||
from
|
||||
{
|
||||
transform: scale(.5);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to
|
||||
{
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.spin
|
||||
{
|
||||
animation-name: spinAnim;
|
||||
animation-timing-function: var(--curveEasyEaseMax);
|
||||
animation-duration: .5s;
|
||||
}
|
||||
|
||||
@keyframes spinAnim
|
||||
{
|
||||
from
|
||||
{
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to
|
||||
{
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fadeIn
|
||||
{
|
||||
animation-name: fadeInAnim;
|
||||
animation-timing-function: var(--curveDecelerateMin);
|
||||
animation-duration: .5s;
|
||||
}
|
||||
|
||||
@keyframes fadeInAnim
|
||||
{
|
||||
from
|
||||
{
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to
|
||||
{
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stack
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.horizontal
|
||||
{
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&.gap
|
||||
{
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from "react";
|
||||
import { Accordion, FluentProvider, Text, Theme, Title2, webDarkTheme, webLightTheme } from "@fluentui/react-components";
|
||||
import "./App.scss";
|
||||
import SettingsSection from "./Components/SettingsSection";
|
||||
import AboutSection from "./Components/AboutSection";
|
||||
import Package from "../package.json";
|
||||
import PasswordView from "./Components/PasswordView";
|
||||
import Settings from "./Utils/Settings";
|
||||
import GeneratorOptions from "./Utils/GeneratorOptions";
|
||||
import { loc } from "./Utils/Localization";
|
||||
|
||||
interface IStates
|
||||
{
|
||||
theme: Theme;
|
||||
generatorOptions: GeneratorOptions;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
interface IProps
|
||||
{
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export default class App extends React.Component<IProps, IStates>
|
||||
{
|
||||
constructor(props: IProps)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
theme: this.UpdateTheme(),
|
||||
generatorOptions: new GeneratorOptions(),
|
||||
settings: props.settings
|
||||
};
|
||||
|
||||
Settings.OnChanged = (changes) => this.setState({ settings: { ...this.state.settings, ...changes } });
|
||||
GeneratorOptions.OnChanged = (changes) => this.setState({ generatorOptions: { ...this.state.generatorOptions, ...changes } });
|
||||
}
|
||||
|
||||
public async componentDidMount(): Promise<void>
|
||||
{
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change",
|
||||
(arg: MediaQueryListEvent) =>
|
||||
this.setState({ theme: this.UpdateTheme(arg.matches) })
|
||||
);
|
||||
|
||||
this.setState({ generatorOptions: await GeneratorOptions.Init() });
|
||||
}
|
||||
|
||||
private UpdateTheme(isDark?: boolean): Theme
|
||||
{
|
||||
let theme: Theme = (isDark ?? window.matchMedia("(prefers-color-scheme: dark)").matches) ? webDarkTheme : webLightTheme;
|
||||
document.body.style.backgroundColor = theme.colorNeutralBackground1;
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
public render(): JSX.Element
|
||||
{
|
||||
return (
|
||||
<FluentProvider theme={ this.state.theme }>
|
||||
<main className="stack gap">
|
||||
<header className="stack horizontal gap">
|
||||
<Title2 as="h1">{ loc("Password generator") }</Title2>
|
||||
<Text as="span">v{ Package.version }</Text>
|
||||
</header>
|
||||
|
||||
<PasswordView settings={ this.state.settings } generatorOptions={ this.state.generatorOptions } />
|
||||
|
||||
<Accordion collapsible>
|
||||
<SettingsSection
|
||||
generatorOptions={ this.state.generatorOptions }
|
||||
settings={ this.state.settings } />
|
||||
<AboutSection />
|
||||
</Accordion>
|
||||
</main>
|
||||
</FluentProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { AccordionItem, AccordionHeader, AccordionPanel, Link, Text, Button } from "@fluentui/react-components";
|
||||
import { InfoRegular, PersonFeedbackRegular } from "@fluentui/react-icons";
|
||||
import { ReactComponent as BuyMeACoffee } from "../Assets/BuyMeACoffee.svg";
|
||||
import React from "react";
|
||||
import { loc } from "../Utils/Localization";
|
||||
import browser from "../Utils/Browser";
|
||||
|
||||
export default class AboutSection extends React.Component
|
||||
{
|
||||
public render(): JSX.Element
|
||||
{
|
||||
return (
|
||||
<AccordionItem value="about">
|
||||
<AccordionHeader as="h2" icon={ <InfoRegular /> }>{ loc("About") }</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<section className="stack gap fadeIn">
|
||||
<Text as="p">
|
||||
{ loc("Developed by Eugene Fox") } (<Link href="https://twitter.com/xfox111" target="_blank">@xfox111</Link>)
|
||||
<br />
|
||||
{ loc("Licensed under") } <Link href="https://github.com/XFox111/PasswordGeneratorExtension/blob/master/LICENSE" target="_blank">{ loc("MIT license") }</Link>
|
||||
</Text>
|
||||
<Text as="p">
|
||||
{ loc("Want to contribute translation for your language?") } <Link href="https://github.com/XFox111/PasswordGeneratorExtension/blob/master/CONTRIBUTING.md" target="_blank">{ loc("Read this to get started") }</Link>
|
||||
</Text>
|
||||
<Text as="p">
|
||||
<Link href="https://xfox111.net/" target="_blank">{ loc("My website") }</Link>
|
||||
<br />
|
||||
<Link href="https://github.com/xfox111/PasswordGeneratorExtension" target="_blank">{ loc("Source code") }</Link>
|
||||
<br />
|
||||
<Link href="https://github.com/XFox111/PasswordGeneratorExtension/releases/latest" target="_blank">{ loc("Changelog") }</Link>
|
||||
</Text>
|
||||
|
||||
<div className="stack horizontal gap">
|
||||
<Button
|
||||
as="a" target="_blank"
|
||||
href={ this.GetFeedbackLink() }
|
||||
appearance="primary" icon={ <PersonFeedbackRegular /> }>
|
||||
|
||||
{ loc("Leave feedback") }
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
as="a" target="_blank"
|
||||
href="https://buymeacoffee.com/xfox111"
|
||||
className="bmc" appearance="primary" icon={ <BuyMeACoffee /> }>
|
||||
|
||||
{ loc("Buy me a coffee") }
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
|
||||
private GetFeedbackLink(): string
|
||||
{
|
||||
let manifest: { [key: string]: string | any } = browser?.runtime?.getManifest();
|
||||
|
||||
if (!manifest)
|
||||
return "mailto:feedback@xfox111.net";
|
||||
else
|
||||
{
|
||||
if (manifest.update_url)
|
||||
{
|
||||
let host: string = new URL(manifest.update_url).host;
|
||||
|
||||
if (host.endsWith("edge.microsoft.com"))
|
||||
return "https://microsoftedge.microsoft.com/addons/detail/password-generator/manimdhobjbkfpeeehlhhneookiokpbj";
|
||||
else if (host.endsWith("google.com"))
|
||||
return "https://chrome.google.com/webstore/detail/password-generator/jnjobgjobffgmgfnkpkjfjkkfhfikmfl";
|
||||
else
|
||||
return "mailto:feedback@xfox111.net";
|
||||
}
|
||||
else
|
||||
return "mailto:feedback@xfox111.net";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Button, Text, Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, DialogContent } from "@fluentui/react-components";
|
||||
import { Table, TableHeader, TableRow, TableHeaderCell, TableBody, TableCell } from "@fluentui/react-components/unstable";
|
||||
import { QuestionCircleRegular } from "@fluentui/react-icons";
|
||||
import React from "react";
|
||||
import Generator from "../Utils/Generator";
|
||||
import { loc } from "../Utils/Localization";
|
||||
|
||||
export default class CharacterHelpDialog extends React.Component
|
||||
{
|
||||
public render(): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button appearance="subtle" style={ { marginLeft: 5 } } icon={ <QuestionCircleRegular /> } />
|
||||
</DialogTrigger>
|
||||
<DialogSurface aria-label="label">
|
||||
<DialogBody>
|
||||
<DialogTitle>{ loc("Character options") }</DialogTitle>
|
||||
<DialogContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>{ loc("Set_name") }</TableHeaderCell>
|
||||
<TableHeaderCell>{ loc("Characters") }</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Lowercase") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.Lowercase.substring(0, 10) }{ loc(", etc.") }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Uppercase") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.Uppercase.substring(0, 10) }{ loc(", etc.") }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Numeric") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.Numeric }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Special symbols") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.SpecialCharacters }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Ambiguous") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.AmbiguousCharacters }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>{ loc("Similar") }</TableCell>
|
||||
<TableCell>
|
||||
<Text font="monospace">{ Generator.SimilarCharacters }</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<DialogTrigger>
|
||||
<Button appearance="secondary">{ loc("OK") }</Button>
|
||||
</DialogTrigger>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import { Input, Button, Link, Text, Tooltip } from "@fluentui/react-components";
|
||||
import { Alert } from "@fluentui/react-components/unstable";
|
||||
import { ArrowClockwiseRegular, CheckmarkRegular, CopyRegular } from "@fluentui/react-icons";
|
||||
import React from "react";
|
||||
import Generator from "../Utils/Generator";
|
||||
import GeneratorOptions from "../Utils/GeneratorOptions";
|
||||
import { loc } from "../Utils/Localization";
|
||||
import Settings from "../Utils/Settings";
|
||||
|
||||
interface IStates
|
||||
{
|
||||
password: string;
|
||||
error?: string;
|
||||
copyIcon: JSX.Element;
|
||||
}
|
||||
|
||||
interface IProps
|
||||
{
|
||||
generatorOptions: GeneratorOptions;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export default class PasswordView extends React.Component<IProps, IStates>
|
||||
{
|
||||
constructor(props: IProps)
|
||||
{
|
||||
super(props);
|
||||
this.state =
|
||||
{
|
||||
password: Generator.GeneratePassword(props.generatorOptions),
|
||||
error: Generator.ValidateProps(props.generatorOptions),
|
||||
copyIcon: <CopyRegular className="scaleUpIn" />,
|
||||
};
|
||||
}
|
||||
|
||||
private OnCopyPassword(password: string): void
|
||||
{
|
||||
console.log("PasswordView.OnCopyPassword");
|
||||
|
||||
if (!document.hasFocus())
|
||||
return;
|
||||
|
||||
window.navigator.clipboard.writeText(password);
|
||||
|
||||
this.setState({ copyIcon: <CheckmarkRegular className="scaleUpIn" /> });
|
||||
setTimeout(() => this.setState({ copyIcon: <CopyRegular className="scaleUpIn" /> }), 3000);
|
||||
}
|
||||
|
||||
private OnRefreshPassword(): void
|
||||
{
|
||||
console.log("PasswordView.OnRefreshPassword");
|
||||
|
||||
let password: string = Generator.GeneratePassword(this.props.generatorOptions);
|
||||
|
||||
this.setState({ password });
|
||||
|
||||
document.querySelector("svg#refresh-btn")?.classList.add("spin");
|
||||
setTimeout(() => document.querySelector("svg#refresh-btn")?.classList.remove("spin"), 600);
|
||||
|
||||
if (this.props.settings.Autocopy)
|
||||
this.OnCopyPassword(password);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>): void
|
||||
{
|
||||
console.log("PasswordView.componentDidUpdate");
|
||||
|
||||
// Converting to JSON is the easiest way to compare objects
|
||||
if (JSON.stringify(prevProps.generatorOptions) === JSON.stringify(this.props.generatorOptions))
|
||||
return;
|
||||
|
||||
let error: string = Generator.ValidateProps(this.props.generatorOptions);
|
||||
let password = Generator.GeneratePassword(this.props.generatorOptions);
|
||||
|
||||
this.setState({ password, error });
|
||||
|
||||
if (!error && this.props.settings.Autocopy)
|
||||
this.OnCopyPassword(password);
|
||||
}
|
||||
|
||||
private AlterSpecialsOnce(useSpecials: boolean): void
|
||||
{
|
||||
console.log("PasswordView.AlterSpecialsOnce", `useSpecials: ${useSpecials}`);
|
||||
|
||||
let options: GeneratorOptions = { ...this.props.generatorOptions, Special: useSpecials, ExcludeAmbiguous: true };
|
||||
|
||||
let error: string = Generator.ValidateProps(options);
|
||||
let password: string = Generator.GeneratePassword(options);
|
||||
|
||||
this.setState({ password, error });
|
||||
|
||||
if (error)
|
||||
setTimeout(() => this.OnRefreshPassword(), 3000);
|
||||
}
|
||||
|
||||
public render(): JSX.Element
|
||||
{
|
||||
return this.state.error ?
|
||||
<Alert intent="error" className="fadeIn">{ this.state.error }</Alert>
|
||||
:
|
||||
<section className="stack fadeIn">
|
||||
<Input
|
||||
value={ this.state.password } readOnly
|
||||
contentAfter={
|
||||
<>
|
||||
<Tooltip content={ loc("Copy") } relationship="label">
|
||||
<Button onClick={ () => this.OnCopyPassword(this.state.password) } appearance="subtle" icon={ this.state.copyIcon } />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={ loc("Generate new") } relationship="label">
|
||||
<Button onClick={ () => this.OnRefreshPassword() } appearance="subtle" icon={ <ArrowClockwiseRegular id="refresh-btn" /> } />
|
||||
</Tooltip>
|
||||
</>
|
||||
} />
|
||||
<Text>
|
||||
{ this.props.generatorOptions.Special ?
|
||||
<Link onClick={ () => this.AlterSpecialsOnce(false) }>
|
||||
{ loc("Exclude special symbols one time") }
|
||||
</Link>
|
||||
:
|
||||
<Link onClick={ () => this.AlterSpecialsOnce(true) }>
|
||||
{ loc("Include special symbols one time") }
|
||||
</Link>
|
||||
}
|
||||
</Text>
|
||||
</section>
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { AccordionItem, AccordionHeader, AccordionPanel, Label, Text, Input, Divider, Checkbox, Tooltip } from "@fluentui/react-components";
|
||||
import { QuestionCircleRegular, SettingsRegular } from "@fluentui/react-icons";
|
||||
import React from "react";
|
||||
import GeneratorOptions from "../Utils/GeneratorOptions";
|
||||
import { loc } from "../Utils/Localization";
|
||||
import Settings from "../Utils/Settings";
|
||||
import CharacterHelpDialog from "./CharacterHelpDialog";
|
||||
|
||||
interface IProps
|
||||
{
|
||||
generatorOptions: GeneratorOptions;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export default class SettingsSection extends React.Component<IProps>
|
||||
{
|
||||
public render(): JSX.Element
|
||||
{
|
||||
let options: GeneratorOptions = this.props.generatorOptions;
|
||||
let settings: Settings = this.props.settings;
|
||||
|
||||
return (
|
||||
<AccordionItem value="settings">
|
||||
<AccordionHeader as="h2" icon={ <SettingsRegular /> }>{ loc("Settings") }</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<section className="stack gap fadeIn">
|
||||
<Label weight="semibold" htmlFor="pwd-length">{ loc("Password length") }</Label>
|
||||
<div className="stack">
|
||||
<Input
|
||||
id="pwd-length"
|
||||
value={ options.Length?.toString() }
|
||||
onChange={ (_, e) => GeneratorOptions.Update({ Length: parseInt(e.value) }) }
|
||||
type="number" min={ 4 } minLength={ 1 } />
|
||||
<Text size={ 200 }>{ loc("Recommended password length") } <b>16-32</b></Text>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="stack">
|
||||
<Text as="h3" weight="semibold">
|
||||
{ loc("Character options") }
|
||||
<CharacterHelpDialog />
|
||||
</Text>
|
||||
|
||||
<Text as="h4">{ loc("Include") }</Text>
|
||||
<div className="stack horizontal">
|
||||
<Checkbox label={ loc("Special symbols") }
|
||||
checked={ options.Special } onChange={ (_, e) => GeneratorOptions.Update({ Special: e.checked as boolean }) } />
|
||||
<Checkbox label={ loc("Numeric") }
|
||||
checked={ options.Numeric } onChange={ (_, e) => GeneratorOptions.Update({ Numeric: e.checked as boolean }) } />
|
||||
<Checkbox label={ loc("Uppercase") }
|
||||
checked={ options.Uppercase } onChange={ (_, e) => GeneratorOptions.Update({ Uppercase: e.checked as boolean }) } />
|
||||
<Checkbox label={ loc("Lowercase") }
|
||||
checked={ options.Lowercase } onChange={ (_, e) => GeneratorOptions.Update({ Lowercase: e.checked as boolean }) } />
|
||||
</div>
|
||||
|
||||
<Text as="h4">{ loc("Exclude") }</Text>
|
||||
<div className="stack horizontal">
|
||||
<Checkbox label={ loc("Similar") }
|
||||
checked={ options.ExcludeSimilar } onChange={ (_, e) => GeneratorOptions.Update({ ExcludeSimilar: e.checked as boolean }) } />
|
||||
<Checkbox label={ loc("Ambiguous") }
|
||||
checked={ options.ExcludeAmbiguous } onChange={ (_, e) => GeneratorOptions.Update({ ExcludeAmbiguous: e.checked as boolean }) } />
|
||||
<Checkbox label={ loc("Repeating") }
|
||||
checked={ options.ExcludeRepeating } onChange={ (_, e) => GeneratorOptions.Update({ ExcludeRepeating: e.checked as boolean }) } />
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="stack">
|
||||
<div>
|
||||
<Tooltip content={ loc("Right-click password field to quickly generate password") } relationship="description">
|
||||
<Checkbox label={ <Text>{ loc("Add shortcut to context menu") } <QuestionCircleRegular /></Text> }
|
||||
checked={ settings.AddContext } onChange={ (_, e) => Settings.Update({ AddContext: e.checked as boolean }) } />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Checkbox label={ loc("Automatically copy to clipboard") }
|
||||
checked={ settings.Autocopy } onChange={ (_, e) => Settings.Update({ Autocopy: e.checked as boolean }) } />
|
||||
</div>
|
||||
</section>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// BackgroundService.ts
|
||||
// Background script that handles the context menu visibility
|
||||
|
||||
import { Tabs, Menus } from "webextension-polyfill";
|
||||
import browser from "../Utils/Browser";
|
||||
import { loc } from "../Utils/Localization";
|
||||
|
||||
function UpdateContextMenu(isEnabled: boolean): void
|
||||
{
|
||||
console.log("BackgroundService.UpdateContextMenu", isEnabled);
|
||||
browser.contextMenus.update("generatePassword", { visible: isEnabled });
|
||||
}
|
||||
|
||||
async function OnContextClick(info: Menus.OnClickData): Promise<void>
|
||||
{
|
||||
console.log("BackgroundService.OnContextClick", info);
|
||||
let tabInfo: Tabs.Tab[] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
console.log("BackgroundService.OnContextClick", tabInfo);
|
||||
|
||||
browser.tabs.sendMessage(tabInfo[0].id, info.menuItemId as string);
|
||||
}
|
||||
|
||||
async function OnInstalled(): Promise<void>
|
||||
{
|
||||
console.log("[BackgroundService] browser.runtime.onInstalled");
|
||||
browser.contextMenus.removeAll();
|
||||
|
||||
browser.contextMenus.create(
|
||||
{
|
||||
title: loc("Quick generate password"),
|
||||
contexts: ["editable"],
|
||||
id: "generatePassword"
|
||||
}
|
||||
);
|
||||
|
||||
let settings: { [key: string]: any; } = await browser.storage.sync.get({ AddContext: true });
|
||||
|
||||
UpdateContextMenu(settings.AddContext);
|
||||
}
|
||||
|
||||
async function OnStorageChanged(changes: any): Promise<void>
|
||||
{
|
||||
console.log("[BackgroundService] browser.storage.sync.onChanged", changes);
|
||||
if (changes.AddContext?.newValue !== undefined)
|
||||
UpdateContextMenu(changes.AddContext.newValue);
|
||||
}
|
||||
|
||||
if (!browser.runtime.onInstalled.hasListener(OnInstalled))
|
||||
browser.runtime.onInstalled.addListener(OnInstalled);
|
||||
|
||||
if (!browser.contextMenus.onClicked.hasListener(OnContextClick))
|
||||
browser.contextMenus.onClicked.addListener(OnContextClick);
|
||||
|
||||
if (!browser.storage.sync.onChanged.hasListener(OnStorageChanged))
|
||||
browser.storage.sync.onChanged.addListener(OnStorageChanged);
|
||||
@@ -1,40 +0,0 @@
|
||||
// ContentService.ts
|
||||
// Content script that handles quick password generation through context menu
|
||||
|
||||
import browser from "../Utils/Browser";
|
||||
import Generator from "../Utils/Generator";
|
||||
import GeneratorOptions from "../Utils/GeneratorOptions";
|
||||
import { loc } from "../Utils/Localization";
|
||||
|
||||
async function OnMessage(message: any): Promise<void>
|
||||
{
|
||||
console.log("[ContentService] browser.runtime.onMessage", message);
|
||||
|
||||
if (message === "generatePassword")
|
||||
{
|
||||
let generatorOptions: GeneratorOptions = await GeneratorOptions.Init();
|
||||
let password: string = Generator.GeneratePassword(generatorOptions);
|
||||
|
||||
let input: HTMLInputElement = document.activeElement as HTMLInputElement;
|
||||
|
||||
if (!["INPUT", "TEXTAREA"].includes(input.tagName))
|
||||
return;
|
||||
|
||||
console.log("[ContentService] browser.runtime.onMessage", input);
|
||||
|
||||
if (input.tagName !== "INPUT" || input.readOnly || !["text", "password"].includes(input.type))
|
||||
{
|
||||
window.alert(loc("Quick generator is only available on password fields"));
|
||||
return;
|
||||
}
|
||||
|
||||
input.focus();
|
||||
input.value = password;
|
||||
window.navigator.clipboard.writeText(password);
|
||||
}
|
||||
}
|
||||
|
||||
if (!browser.runtime.onMessage.hasListener(OnMessage))
|
||||
browser.runtime.onMessage.addListener(OnMessage);
|
||||
|
||||
console.log("[ContentService] Loaded");
|
||||
@@ -1,4 +0,0 @@
|
||||
import Browser from "webextension-polyfill";
|
||||
|
||||
const browser: typeof Browser = (process.env.NODE_ENV !== "development") ? require("webextension-polyfill") : null;
|
||||
export default browser;
|
||||
@@ -1,119 +0,0 @@
|
||||
import GeneratorOptions from "./GeneratorOptions";
|
||||
import { loc } from "./Localization";
|
||||
|
||||
export default class Generator
|
||||
{
|
||||
public static Uppercase = "ABCDEFGHJKMNPQRSTUVWXYZ";
|
||||
public static Lowercase = this.Uppercase.toLowerCase();
|
||||
public static Numeric = "23456789";
|
||||
public static SpecialCharacters = "!#$%&*+-=?@^_";
|
||||
public static AmbiguousCharacters = "{}[]()/\\'\"`~,;:.<>";
|
||||
public static SimilarCharacters = "il1Lo0O";
|
||||
|
||||
public static GeneratePassword(props: GeneratorOptions): string
|
||||
{
|
||||
if (!props.Length || isNaN(props.Length) || props.Length < 4)
|
||||
props.Length = 4;
|
||||
|
||||
// Validating parameters
|
||||
if (this.ValidateProps(props))
|
||||
return "";
|
||||
|
||||
// Generating password
|
||||
let availableCharacters: string = this.GetAvailableCharacters(props);
|
||||
let requiredCharacters: string = this.GetRequiredCharacters(props);
|
||||
|
||||
let password: string = "";
|
||||
|
||||
for (let i = 0; i < props.Length; i++)
|
||||
{
|
||||
let char: string = this.PickRandomFromArray(availableCharacters);
|
||||
|
||||
if (props.ExcludeRepeating && password.includes(char))
|
||||
i--;
|
||||
else
|
||||
password += char;
|
||||
}
|
||||
|
||||
for (let i = 0; i < requiredCharacters.length; i++)
|
||||
{
|
||||
if (props.ExcludeRepeating && password.includes(requiredCharacters[i]))
|
||||
continue;
|
||||
|
||||
password = password.replace(this.PickRandomFromArray(password), requiredCharacters[i]);
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
public static ValidateProps(props: GeneratorOptions): string
|
||||
{
|
||||
if (!props.Length || isNaN(props.Length) || props.Length < 4)
|
||||
props.Length = 4;
|
||||
|
||||
if (!props.Lowercase && !props.Uppercase)
|
||||
return loc("Either lowercase or uppercase characters must be included");
|
||||
|
||||
let availableCharacters: string = this.GetAvailableCharacters(props);
|
||||
|
||||
if (props.ExcludeRepeating && availableCharacters.length < props.Length)
|
||||
return loc("Selected length is too long to exclude repeating characters");
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static GetAvailableCharacters(props: GeneratorOptions): string
|
||||
{
|
||||
let availableCharacters: string = "";
|
||||
|
||||
if (props.Special)
|
||||
availableCharacters += this.SpecialCharacters;
|
||||
if (props.Numeric)
|
||||
availableCharacters += this.Numeric;
|
||||
if (props.Lowercase)
|
||||
availableCharacters += this.Lowercase;
|
||||
if (props.Uppercase)
|
||||
availableCharacters += this.Uppercase;
|
||||
|
||||
if (!props.ExcludeAmbiguous)
|
||||
availableCharacters += this.AmbiguousCharacters;
|
||||
if (!props.ExcludeSimilar)
|
||||
availableCharacters += this.SimilarCharacters;
|
||||
|
||||
return availableCharacters;
|
||||
}
|
||||
|
||||
private static GetRequiredCharacters(props: GeneratorOptions): string
|
||||
{
|
||||
let requiredCharacters: string = "";
|
||||
|
||||
if (props.Special)
|
||||
requiredCharacters += this.PickRandomFromArray(this.SpecialCharacters);
|
||||
if (props.Numeric)
|
||||
requiredCharacters += this.PickRandomFromArray(this.Numeric);
|
||||
if (props.Lowercase)
|
||||
requiredCharacters += this.PickRandomFromArray(this.Lowercase);
|
||||
if (props.Uppercase)
|
||||
requiredCharacters += this.PickRandomFromArray(this.Uppercase);
|
||||
|
||||
if (!props.ExcludeAmbiguous)
|
||||
requiredCharacters += this.PickRandomFromArray(this.AmbiguousCharacters);
|
||||
if (!props.ExcludeSimilar)
|
||||
requiredCharacters += this.PickRandomFromArray(this.SimilarCharacters);
|
||||
|
||||
return requiredCharacters;
|
||||
}
|
||||
|
||||
// See https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/random
|
||||
// min is inclusive, max is exclusive
|
||||
private static GetRandomInt(min: number, max: number): number
|
||||
{
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
private static PickRandomFromArray(array: string): string
|
||||
{
|
||||
return array[this.GetRandomInt(0, array.length)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Storage } from "webextension-polyfill";
|
||||
import browser from "../Utils/Browser";
|
||||
|
||||
export default class GeneratorOptions
|
||||
{
|
||||
public Length: number = 16;
|
||||
|
||||
public Special: boolean = true;
|
||||
public Numeric: boolean = true;
|
||||
public Lowercase: boolean = true;
|
||||
public Uppercase: boolean = true;
|
||||
|
||||
public ExcludeSimilar: boolean = true;
|
||||
public ExcludeAmbiguous: boolean = true;
|
||||
public ExcludeRepeating: boolean = false;
|
||||
|
||||
public static OnChanged: (changes: Partial<GeneratorOptions>) => void;
|
||||
|
||||
public static async Init(): Promise<GeneratorOptions>
|
||||
{
|
||||
let fallbackOptions: GeneratorOptions = new GeneratorOptions();
|
||||
|
||||
if (!browser?.storage?.sync) // Extension is running as a standalone app
|
||||
return fallbackOptions;
|
||||
|
||||
let props: { [key: string]: any; } = await browser.storage.sync.get(fallbackOptions);
|
||||
|
||||
browser.storage.sync.onChanged.addListener(GeneratorOptions.OnStorageChanged);
|
||||
|
||||
return props as GeneratorOptions;
|
||||
}
|
||||
|
||||
public static async Update(changes: Partial<GeneratorOptions>): Promise<void>
|
||||
{
|
||||
if (browser?.storage?.sync)
|
||||
await browser?.storage?.sync?.set(changes);
|
||||
else
|
||||
GeneratorOptions.OnChanged(changes);
|
||||
}
|
||||
|
||||
private static OnStorageChanged(changes: { [key: string]: Storage.StorageChange; }): void
|
||||
{
|
||||
let propsList: string[] = Object.keys(new GeneratorOptions());
|
||||
let options: { [key: string]: any; } = {};
|
||||
|
||||
Object.entries(changes)
|
||||
.filter(i => propsList.includes(i[0]))
|
||||
.map(i => options[i[0]] = i[1].newValue);
|
||||
|
||||
if (GeneratorOptions.OnChanged)
|
||||
GeneratorOptions.OnChanged(options as Partial<GeneratorOptions>);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import browser from "../Utils/Browser";
|
||||
|
||||
export default class Localization
|
||||
{
|
||||
public static GetString(key: string): string
|
||||
{
|
||||
let sanitizedKey: string = key
|
||||
.replaceAll(".", "_")
|
||||
.replaceAll(",", "_")
|
||||
.replaceAll(" ", "_")
|
||||
.replaceAll("-", "_")
|
||||
.replaceAll("?", "_")
|
||||
.replaceAll("!", "_");
|
||||
|
||||
let str: string = browser?.i18n?.getMessage(sanitizedKey);
|
||||
|
||||
return str ?? key;
|
||||
}
|
||||
}
|
||||
|
||||
export function loc(key: string): string
|
||||
{
|
||||
return Localization.GetString(key);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Storage } from "webextension-polyfill";
|
||||
import browser from "../Utils/Browser";
|
||||
|
||||
export default class Settings
|
||||
{
|
||||
public AddContext: boolean = true;
|
||||
public Autocopy: boolean = true;
|
||||
|
||||
public static OnChanged: (changes: Partial<Settings>) => void;
|
||||
|
||||
public static async Init(): Promise<Settings>
|
||||
{
|
||||
let fallbackOptions = new Settings();
|
||||
|
||||
if (!browser?.storage?.sync)
|
||||
return fallbackOptions;
|
||||
|
||||
let props: { [key: string]: any; } = await browser.storage.sync.get(fallbackOptions);
|
||||
|
||||
browser.storage.sync.onChanged.addListener(Settings.OnStorageChanged);
|
||||
|
||||
return props as Settings;
|
||||
}
|
||||
|
||||
public static async Update(changes: Partial<Settings>): Promise<void>
|
||||
{
|
||||
if (browser?.storage?.sync)
|
||||
await browser?.storage?.sync?.set(changes);
|
||||
else
|
||||
Settings.OnChanged(changes);
|
||||
}
|
||||
|
||||
private static OnStorageChanged(changes: { [key: string]: Storage.StorageChange; }): void
|
||||
{
|
||||
let propsList: string[] = Object.keys(new Settings());
|
||||
let settings: { [key: string]: any; } = {};
|
||||
|
||||
Object.entries(changes)
|
||||
.filter(i => propsList.includes(i[0]))
|
||||
.map(i => settings[i[0]] = i[1].newValue);
|
||||
|
||||
if (Settings.OnChanged)
|
||||
Settings.OnChanged(settings as Partial<Settings>);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import Settings from "./Utils/Settings";
|
||||
|
||||
Settings.Init().then(settings =>
|
||||
ReactDOM
|
||||
.createRoot(document.querySelector("#root") as HTMLElement)
|
||||
.render(<App settings={ settings } />));
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -1,27 +1,7 @@
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BrandVariants, Theme, createDarkTheme, createLightTheme } from "@fluentui/react-components";
|
||||
|
||||
const bmcBrandRamp: BrandVariants =
|
||||
{
|
||||
"10": "#050201",
|
||||
"20": "#20140C",
|
||||
"30": "#372014",
|
||||
"40": "#492918",
|
||||
"50": "#5C321D",
|
||||
"60": "#6F3C21",
|
||||
"70": "#834525",
|
||||
"80": "#984F2A",
|
||||
"90": "#AD5A2E",
|
||||
"100": "#C36433",
|
||||
"110": "#D96E37",
|
||||
"120": "#EF793C",
|
||||
"130": "#FF884A",
|
||||
"140": "#FFA170",
|
||||
"150": "#FFB792",
|
||||
"160": "#FFCCB3"
|
||||
};
|
||||
|
||||
export const bmcLightTheme: Theme =
|
||||
{
|
||||
...createLightTheme(bmcBrandRamp),
|
||||
colorBrandBackground: bmcBrandRamp[110],
|
||||
};
|
||||
|
||||
export const bmcDarkTheme: Theme =
|
||||
{
|
||||
...createDarkTheme(bmcBrandRamp),
|
||||
colorBrandBackground: bmcBrandRamp[120],
|
||||
colorBrandForeground1: bmcBrandRamp[110],
|
||||
colorBrandForeground2: bmcBrandRamp[120]
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||